@chrisdudek/yg 4.0.2 → 4.2.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 +925 -533
- package/graph-schemas/yg-architecture.yaml +18 -6
- package/graph-schemas/yg-aspect.yaml +51 -5
- package/graph-schemas/yg-flow.yaml +17 -4
- package/graph-schemas/yg-node.yaml +26 -12
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -28,6 +28,13 @@ You verify whether source code satisfies a requirement.
|
|
|
28
28
|
Below is a node (component) with its source files and one aspect (rule set).
|
|
29
29
|
Check every rule in the aspect against the source code.
|
|
30
30
|
|
|
31
|
+
If source code contains a comment with the marker yg-suppress(<aspect-id>) where
|
|
32
|
+
<aspect-id> matches the aspect you are checking, treat the suppressed code as satisfied.
|
|
33
|
+
The marker must include a reason after the closing parenthesis. Do not validate the
|
|
34
|
+
reason \u2014 accept it as-is. The marker applies contextually to the surrounding code
|
|
35
|
+
(function, class, or block where it appears). If placed at file level, it applies to
|
|
36
|
+
the entire file.
|
|
37
|
+
|
|
31
38
|
Respond with EXACTLY this JSON, nothing else:
|
|
32
39
|
{"satisfied": true|false, "reason": "explanation with file:line references"}
|
|
33
40
|
</task>
|
|
@@ -118,6 +125,7 @@ import { Command } from "commander";
|
|
|
118
125
|
|
|
119
126
|
// src/cli/init.ts
|
|
120
127
|
import chalk from "chalk";
|
|
128
|
+
import { existsSync } from "fs";
|
|
121
129
|
import { mkdir as mkdir2, writeFile as writeFile4, readdir as readdir2, readFile as readFile4, stat as stat2 } from "fs/promises";
|
|
122
130
|
import path5 from "path";
|
|
123
131
|
import { fileURLToPath } from "url";
|
|
@@ -153,437 +161,237 @@ import { readFile, writeFile, mkdir } from "fs/promises";
|
|
|
153
161
|
import path from "path";
|
|
154
162
|
|
|
155
163
|
// src/templates/rules.ts
|
|
156
|
-
var
|
|
157
|
-
|
|
158
|
-
<EXTREMELY-IMPORTANT>
|
|
159
|
-
This is your operating manual for working in a Yggdrasil-managed repository.
|
|
160
|
-
|
|
161
|
-
<critical_protocol>
|
|
162
|
-
BEFORE reading, analyzing, or modifying ANY source file:
|
|
163
|
-
\`yg context --file <path>\`
|
|
164
|
-
Resolves owner, gives you the aspects this file must satisfy.
|
|
165
|
-
Read the aspect content \`.md\` files \u2014 those are the rules the reviewer enforces.
|
|
166
|
-
|
|
167
|
-
BEFORE creating a NEW source file:
|
|
168
|
-
Identify which existing node the new file belongs to (by intent, not by filename).
|
|
169
|
-
Run \`yg context --node <node-path>\` to load the context \u2014 especially aspect rules the new file must follow.
|
|
170
|
-
If the file doesn't fit an existing node, create the node first (Step 2b below).
|
|
171
|
-
If unsure which node: run \`yg context --file <path>\` \u2014 the CLI will list candidate nodes from the same directory.
|
|
172
|
-
New files without graph context are the #1 source of convention violations.
|
|
164
|
+
var SYSTEM = `## SYSTEM
|
|
173
165
|
|
|
174
|
-
|
|
175
|
-
The graph captures architectural constraints that source files cannot \u2014 without it, you will write code that violates cross-cutting requirements.
|
|
176
|
-
</critical_protocol>
|
|
166
|
+
Yggdrasil is continuous architecture enforcement. A graph in \`.yggdrasil/\` describes the architecture. An LLM reviewer verifies source code against it at approve time. If code violates a rule, the reviewer rejects it.
|
|
177
167
|
|
|
178
|
-
|
|
179
|
-
</EXTREMELY-IMPORTANT>
|
|
168
|
+
The CLI (\`yg\`) reads and validates \u2014 it never modifies files. You create and edit graph files manually. The CLI guides you: every error message says WHAT happened, WHY it matters, and the NEXT command to run. \`suggestedNext\` at the end of \`yg check\` gives one concrete step. Follow it.
|
|
180
169
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
### Quick Start
|
|
170
|
+
### Graph Elements
|
|
184
171
|
|
|
185
172
|
\`\`\`
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
For blast radius: also run yg impact --file <path>.
|
|
195
|
-
|
|
196
|
-
AFTER modifying:
|
|
197
|
-
yg check \u2014 fix all errors
|
|
198
|
-
yg approve --node <owner> \u2014 reviewer verifies aspects vs source code
|
|
199
|
-
|
|
200
|
-
ALWAYS: establish graph coverage before modifying code.
|
|
201
|
-
ALWAYS: run yg context --file before reading source.
|
|
202
|
-
ALWAYS: run yg impact before assessing blast radius.
|
|
203
|
-
ALWAYS: ask before resolving ambiguity.
|
|
204
|
-
WHEN UNSURE: ask the user. Do not guess. Do not assume.
|
|
205
|
-
|
|
206
|
-
How CLI guides you:
|
|
207
|
-
Every error message follows: WHAT happened \u2192 WHY it's a problem \u2192 NEXT command.
|
|
208
|
-
suggestedNext at the end of check gives one concrete step + remaining scale.
|
|
209
|
-
Follow it. Re-run check after each fix.
|
|
173
|
+
.yggdrasil/
|
|
174
|
+
yg-architecture.yaml \u2190 node type definitions, default aspects per type, allowed relations
|
|
175
|
+
yg-config.yaml \u2190 reviewer config, quality thresholds, parallelism
|
|
176
|
+
model/ \u2190 nodes: what exists \u2014 hierarchy, relations, file mappings
|
|
177
|
+
aspects/ \u2190 aspects: what must be satisfied \u2014 enforceable rules
|
|
178
|
+
flows/ \u2190 flows: business processes with node participation
|
|
179
|
+
schemas/ \u2190 YAML schemas \u2014 read before creating any graph element
|
|
180
|
+
.drift-state/ \u2190 generated by CLI; never edit manually
|
|
210
181
|
\`\`\`
|
|
211
182
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
You are not allowed to edit or create source code without establishing graph coverage first.
|
|
215
|
-
|
|
216
|
-
**Step 1** \u2014 Get context: \`yg context --file <path>\` (resolves owner automatically)
|
|
217
|
-
|
|
218
|
-
**Step 2a** \u2014 Owner found: execute checklist:
|
|
219
|
-
|
|
220
|
-
- [ ] 1. \`yg context --file <path>\` \u2014 note all aspects in "Must satisfy"
|
|
221
|
-
- [ ] 2. **Read aspect content files.** For every aspect in "Must satisfy": open and read its content \`.md\` files. The aspect description is not sufficient \u2014 the content files contain the actual enforcement rules. \`yg approve\` (step 6) delegates to a reviewer that checks source code against these rules and rejects non-compliant code.
|
|
222
|
-
- [ ] 3. Assess blast radius: \`yg impact --node <node_path>\`
|
|
223
|
-
- [ ] 4. Modify source code \u2014 satisfy the aspect rules
|
|
224
|
-
- [ ] 5. Run \`yg check\` \u2014 follow CLI's suggested next command (if unfixable after 3 attempts \u2192 stop, report to user)
|
|
225
|
-
- [ ] 5b. If you split, merged, or renamed a node: run \`yg flows\` and update any flow \`nodes\` lists that referenced the old node path.
|
|
226
|
-
- [ ] 5c. **Aspect check** \u2014 did you just apply a pattern that also exists in other files? If the node has no aspect for it and you saw the same pattern in 3+ files, create the aspect now.
|
|
227
|
-
- [ ] 6. Run \`yg approve --node <node_path>\` \u2014 reviewer verifies aspects vs source code
|
|
228
|
-
|
|
229
|
-
**Step 2b** \u2014 Owner not found: establish coverage first. Present options to the user:
|
|
230
|
-
|
|
231
|
-
*Partially mapped* (file unmapped but inside a mapped module): ask whether to add to existing node or create new one.
|
|
232
|
-
|
|
233
|
-
*Existing code:*
|
|
183
|
+
**Nodes** \u2014 components. \`model/<path>/yg-node.yaml\` with name, type, description, mapping (source files), relations, aspects, ports. Nodes nest by directory \u2014 children inherit parent aspects.
|
|
234
184
|
|
|
235
|
-
|
|
236
|
-
- Option B \u2014 Abort
|
|
185
|
+
**Aspects** \u2014 enforceable rules. \`aspects/<id>/yg-aspect.yaml\` + content \`.md\` files. The content files are what the reviewer checks against source code. An aspect can declare \`implies: [other-aspect]\` \u2014 implied aspects are included recursively (must be acyclic).
|
|
237
186
|
|
|
238
|
-
|
|
187
|
+
**Flows** \u2014 business processes. \`flows/<name>/yg-flow.yaml\` with name, description, nodes (participants), aspects. Flow-level aspects propagate to all participants. Descendants of a declared participant are automatically included \u2014 adding a parent node to a flow covers all its children.
|
|
239
188
|
|
|
240
|
-
|
|
241
|
-
2. Create flows if the code participates in a business process (with flow-level aspects)
|
|
242
|
-
3. Create nodes: \`yg-node.yaml\` with description, mapping, relations, aspects
|
|
243
|
-
4. Review the context package (\`yg context\`) \u2014 aspects are the specification
|
|
244
|
-
5. Implement code that satisfies aspect rules. Every source file must be mapped.
|
|
245
|
-
6. \`yg check\`, \`yg approve\`
|
|
189
|
+
**Relations** \u2014 typed dependencies between nodes. Six types: \`calls\`, \`uses\`, \`extends\`, \`implements\` (structural) and \`emits\`, \`listens\` (event-based). Event relations must be paired \u2014 if A emits to B, B must have a listens from A. Architecture controls which relation types are allowed between which node types.
|
|
246
190
|
|
|
247
|
-
**
|
|
191
|
+
**Ports** \u2014 named entry points on a node with required aspects. A node declares ports to say "consumers of this endpoint must satisfy these aspects." Consumers reference ports via \`consumes\` on their relation. The consumed port's aspects become effective on the consumer (channel 6). If a target has ports, the consumer must declare which it consumes \u2014 otherwise check warns about missing port contracts.
|
|
248
192
|
|
|
249
|
-
**
|
|
193
|
+
**Architecture** \u2014 \`yg-architecture.yaml\` defines the vocabulary: node types, default aspects per type, allowed parent types, allowed relation targets per type. This is the foundation \u2014 read it when starting work on a new repo. Changes require user confirmation. Structure details in \`schemas/yg-architecture.yaml\`.
|
|
250
194
|
|
|
251
|
-
|
|
195
|
+
### How Aspects Reach a Node \u2014 7 Channels + Applicability Filter
|
|
252
196
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
### Working from External Specifications
|
|
256
|
-
|
|
257
|
-
When the user provides external documents (specs, PRDs, design docs, reference docs) as input for implementation:
|
|
258
|
-
|
|
259
|
-
1. **Read ALL spec documents BEFORE writing any code.** Understand the full scope.
|
|
260
|
-
2. **Extract enforceable requirements as aspects FIRST** \u2014 these are the rules the reviewer will check.
|
|
261
|
-
3. **The graph enforces architecture; external docs are INPUT to the graph, not a parallel source of truth.**
|
|
262
|
-
4. **Non-enforceable knowledge** (business strategy, personas, pricing) is not captured in the graph. Enforceable rules go to aspects.
|
|
263
|
-
|
|
264
|
-
### Conversation Lifecycle
|
|
197
|
+
Aspects accumulate from multiple sources simultaneously. The reviewer checks ALL of them \u2014 the node must satisfy every aspect regardless of origin.
|
|
265
198
|
|
|
266
199
|
\`\`\`
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
-
|
|
283
|
-
-
|
|
200
|
+
EXAMPLE: node "orders/handler" (type: command, child of "orders")
|
|
201
|
+
|
|
202
|
+
Channel 1: OWN \u2014 node.aspects: [input-validation]
|
|
203
|
+
Channel 2: ANCESTOR \u2014 parent "orders" has aspects: [audit-logging]
|
|
204
|
+
Channel 3: OWN TYPE \u2014 architecture says type "command" \u2192 [cli-command-contract]
|
|
205
|
+
Channel 4: ANCESTOR TYPE \u2014 parent "orders" type "module" \u2192 [] (no defaults here)
|
|
206
|
+
Channel 5: FLOWS \u2014 flow "order-processing" includes "orders" \u2192 flow aspects: [deterministic]
|
|
207
|
+
Channel 6: PORTS \u2014 relation to "payments/service" consumes port "charge" \u2192 [correlation-tracking]
|
|
208
|
+
Channel 7: IMPLIED \u2014 aspect "audit-logging" implies: [diagnostic-logging]
|
|
209
|
+
|
|
210
|
+
EFFECTIVE ASPECTS for "orders/handler":
|
|
211
|
+
input-validation \u2190 own
|
|
212
|
+
audit-logging \u2190 parent "orders"
|
|
213
|
+
cli-command-contract \u2190 architecture type "command"
|
|
214
|
+
deterministic \u2190 flow "order-processing" (via parent "orders")
|
|
215
|
+
correlation-tracking \u2190 port "charge" on "payments/service"
|
|
216
|
+
diagnostic-logging \u2190 implied by "audit-logging"
|
|
284
217
|
\`\`\`
|
|
285
218
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
-
|
|
289
|
-
-
|
|
290
|
-
-
|
|
291
|
-
- [ ] 4. Run \`yg check\` immediately \u2014 fix all errors
|
|
292
|
-
- [ ] 5. Verify affected source files are consistent \u2014 update if needed
|
|
293
|
-
- [ ] 6. Run \`yg approve\` for affected nodes
|
|
294
|
-
|
|
295
|
-
### Architecture Ownership
|
|
219
|
+
Consequences of this cascade:
|
|
220
|
+
- Adding an aspect to a parent applies it to ALL children. Check impact first: \`yg impact --aspect <id>\`.
|
|
221
|
+
- Adding a node to a flow with aspects means that node must satisfy flow aspects.
|
|
222
|
+
- Architecture default aspects apply to every node of that type automatically.
|
|
223
|
+
- Implies chains expand recursively. Cycles are forbidden \u2014 CLI detects them.
|
|
296
224
|
|
|
297
|
-
|
|
225
|
+
### Applicability Filter (\`when\`) \u2014 Evaluated on Every Channel
|
|
298
226
|
|
|
299
|
-
|
|
227
|
+
Seven channels propagate aspects onto nodes. A separate mechanism \u2014 the \`when\`
|
|
228
|
+
predicate \u2014 filters applicability. Every channel's attachment passes through
|
|
229
|
+
\`when\` before the aspect becomes effective. The predicate is evaluated by the
|
|
230
|
+
CLI against the graph (deterministic, no LLM call); if false, the aspect is
|
|
231
|
+
silently skipped on that node.
|
|
300
232
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
233
|
+
\`when\` can be declared globally on the aspect (applies across channels) or
|
|
234
|
+
per attach site (per channel instance). Global and attach-site \`when\` combine
|
|
235
|
+
via AND on each channel path. Multiple channels deliver independently \u2014 the
|
|
236
|
+
aspect is effective if any channel's path passes both its global and
|
|
237
|
+
attach-site filter.
|
|
305
238
|
|
|
306
|
-
|
|
239
|
+
Use \`when\` when an aspect is meaningful for only a subset of nodes under a
|
|
240
|
+
common attach channel. Example: \`external-api-error-mapping\` attached to
|
|
241
|
+
type \`command\` but only applicable when the node calls \`service-client\` \u2014
|
|
242
|
+
declare on the aspect:
|
|
307
243
|
|
|
308
|
-
|
|
244
|
+
when:
|
|
245
|
+
relations:
|
|
246
|
+
calls:
|
|
247
|
+
target_type: service-client
|
|
309
248
|
|
|
310
|
-
|
|
249
|
+
Domain-neutral examples: a \`pii-encryption\` aspect attached to all
|
|
250
|
+
repositories, but only applicable when the repository stores a field of
|
|
251
|
+
type \`user-profile\`; an \`idempotency-key\` aspect required only for commands
|
|
252
|
+
that emit events; a \`database-migration-review\` aspect only for nodes with
|
|
253
|
+
\`has_mapping: true\` pointing at \`db/migrations/\`.
|
|
311
254
|
|
|
312
|
-
|
|
313
|
-
2. Follow the Quick Start Protocol from that file before touching any mapped code
|
|
255
|
+
### Reviewer
|
|
314
256
|
|
|
315
|
-
|
|
257
|
+
The reviewer is an LLM invoked by \`yg approve\`. It receives: the aspect's content.md + all source files of the node. It checks every rule from content.md against the code.
|
|
316
258
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
DELIVERABLES \u2014 all required, incomplete work will be rejected:
|
|
320
|
-
1. Working source code
|
|
321
|
-
2. Graph nodes for every new/modified source file
|
|
322
|
-
3. \`yg check\` passing
|
|
323
|
-
\`\`\`
|
|
259
|
+
- **Approved** \u2192 baseline recorded, drift cleared.
|
|
260
|
+
- **Refused** \u2192 violation report with what and where. Fix the code, re-run approve.
|
|
324
261
|
|
|
325
|
-
|
|
326
|
-
var REFERENCE = `## REFERENCE
|
|
262
|
+
Three approve modes: \`--node <path>\` (one or more nodes), \`--aspect <id>\` (batch all nodes affected by this aspect change), \`--flow <name>\` (batch all nodes in this flow). Batch at most 3-5 nodes per invocation \u2014 the reviewer loses accuracy with too many. Use \`--dry-run\` to preview the reviewer prompt without making an LLM call.
|
|
327
263
|
|
|
328
|
-
###
|
|
264
|
+
### Drift and Cascade
|
|
329
265
|
|
|
330
|
-
|
|
331
|
-
.yggdrasil/
|
|
332
|
-
yg-config.yaml \u2190 project config: reviewer, quality thresholds, parallel
|
|
333
|
-
yg-architecture.yaml \u2190 node type definitions, default aspects per type
|
|
334
|
-
model/ \u2190 what exists: nodes, hierarchy, relations, file mappings
|
|
335
|
-
aspects/ \u2190 what must: cross-cutting requirements \u2014 the ONLY enforcement rules
|
|
336
|
-
flows/ \u2190 why and in what process: business processes with node participation
|
|
337
|
-
schemas/ \u2190 YAML schemas \u2014 read before creating any graph element
|
|
338
|
-
.drift-state/ \u2190 generated by CLI; never edit manually
|
|
339
|
-
\`\`\`
|
|
266
|
+
Drift = source code or upstream context changed since the last approve. The reviewer must verify again. \`yg check\` detects two kinds:
|
|
340
267
|
|
|
341
|
-
|
|
268
|
+
- **Source drift** \u2014 mapped source files were modified. Fix: \`yg approve --node <path>\`.
|
|
269
|
+
- **Upstream drift (cascade)** \u2014 an aspect, parent node, flow, or dependency changed. This cascades: one aspect change can cause drift in every node that uses it. Fix: \`yg approve --aspect <id>\` or approve affected nodes individually.
|
|
342
270
|
|
|
343
|
-
|
|
344
|
-
- **Aspect id = directory path** under \`aspects/\`. Each aspect has \`yg-aspect.yaml\` + content \`.md\` files. Content files contain enforcement rules checked by the reviewer. No automatic parent-child \u2014 use \`implies\` explicitly.
|
|
345
|
-
- **Flows = business processes.** A flow describes what happens in the world, not code sequences. Flow aspects propagate to all participants.
|
|
346
|
-
- **Nodes = \`yg-node.yaml\` only.** Name, type, description, mapping, relations, aspects, ports. No \`.md\` files in nodes.
|
|
271
|
+
Cascade is the cost multiplier. Before changing a widely-used aspect, run \`yg impact --aspect <id>\` to see how many nodes will need re-approval. Each is a separate LLM call.
|
|
347
272
|
|
|
348
|
-
|
|
273
|
+
If you modify code without reading the aspect content files (\`yg context --file\` \u2192 follow the \`read:\` paths), you will likely write code that violates rules you didn't know about. The reviewer will reject it. You will have to read the aspects anyway, then rewrite. Double cost.
|
|
349
274
|
|
|
350
|
-
|
|
275
|
+
Do not interrupt \`yg approve\` \u2014 it processes each aspect across all source files. Interrupting leaves drift state unrecorded. Always read the full raw output \u2014 no \`| grep\`, \`| head\`, \`| tail\`. The reviewer already ran; the output is the return on that cost.
|
|
351
276
|
|
|
352
|
-
|
|
277
|
+
### CLI Commands
|
|
353
278
|
|
|
354
|
-
|
|
|
279
|
+
| Command | Purpose |
|
|
355
280
|
|---|---|
|
|
356
|
-
|
|
|
357
|
-
|
|
|
358
|
-
|
|
|
359
|
-
|
|
|
360
|
-
|
|
|
281
|
+
| \`yg check\` | Unified gate \u2014 drift, validation, coverage, completeness. Blocks CI. |
|
|
282
|
+
| \`yg context --file <path>\` | Show owning node, effective aspects (\`read:\` paths), dependencies |
|
|
283
|
+
| \`yg context --node <path>\` | Show node overview \u2014 aspects, flows, dependents, source files |
|
|
284
|
+
| \`yg approve --node <path> [<path2>...]\` | Run reviewer on one or more nodes |
|
|
285
|
+
| \`yg approve --aspect <id>\` | Batch approve all nodes affected by this aspect change |
|
|
286
|
+
| \`yg approve --flow <name>\` | Batch approve all nodes in this flow |
|
|
287
|
+
| \`yg approve --dry-run --node <path>\` | Preview reviewer prompt without LLM call |
|
|
288
|
+
| \`yg impact --node <path>\` | Blast radius \u2014 dependents, flows, cascade scope |
|
|
289
|
+
| \`yg impact --file <path>\` | Blast radius for a specific file |
|
|
290
|
+
| \`yg impact --aspect <id>\` | All nodes affected by this aspect |
|
|
291
|
+
| \`yg impact --flow <name>\` | All nodes in this flow |
|
|
292
|
+
| \`yg tree [--root <path>] [--depth <n>]\` | Browse graph structure \u2014 all nodes with type and description |
|
|
293
|
+
| \`yg aspects\` | List all aspects with usage counts and sources |
|
|
294
|
+
| \`yg flows\` | List all flows with participants and aspects |
|
|
295
|
+
| \`yg owner --file <path>\` | Find which node owns a source file |
|
|
296
|
+
| \`yg init\` | Bootstrap or refresh \`.yggdrasil/\` setup |
|
|
361
297
|
|
|
362
|
-
###
|
|
298
|
+
### Impact and Cost
|
|
363
299
|
|
|
364
|
-
|
|
300
|
+
Every graph change has blast radius. \`yg impact\` shows how many nodes are affected. Each affected node is a separate reviewer call (LLM request) during approve. An aspect touching 20 nodes = 20 LLM calls = real cost.
|
|
365
301
|
|
|
366
|
-
|
|
367
|
-
- **\`yg context --file <path>\`** \u2014 per-file: aspects to satisfy, consumed dependencies
|
|
302
|
+
When code doesn't match an aspect, three options:
|
|
368
303
|
|
|
369
|
-
|
|
304
|
+
| Option | When | Cost |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| **Change code** \u2014 conform to aspect | Aspect is correct, code violates it | Proportional to files needing fixes |
|
|
307
|
+
| **Change aspect** \u2014 conform to code | Aspect is too narrow or wrong, code is correct | \`yg impact --aspect\` \u2192 re-approve ALL nodes with this aspect |
|
|
308
|
+
| **Suppress** \u2014 \`yg-suppress\` waiver | Known tech debt, refactor not now | Zero approve cost, consciously accepted risk |
|
|
370
309
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
- **Source files** \u2014 files owned by this node
|
|
374
|
-
- **Must satisfy** \u2014 aspects with paths to content.md files
|
|
375
|
-
- **Participates in** \u2014 flows
|
|
376
|
-
- **Dependencies** \u2014 nodes this node depends on
|
|
377
|
-
- **Dependents** \u2014 count of nodes that depend on this one (consequence framing for blast radius)
|
|
378
|
-
- **Parent** \u2014 parent node
|
|
310
|
+
This is a cost/impact trade-off. Assess, propose the option to the user, let them decide. Never choose silently \u2014 especially for options 2 and 3.`;
|
|
311
|
+
var DECISIONS = `## DECISIONS
|
|
379
312
|
|
|
380
|
-
|
|
381
|
-
- **Owner** \u2014 node path and type (or "unmapped" with candidate nodes)
|
|
382
|
-
- **Must satisfy** \u2014 aspects with paths to content.md files
|
|
383
|
-
- **Dependencies consumed** \u2014 what this file uses from each dependency
|
|
384
|
-
- **Node context** \u2014 back-pointer: run \`yg context --node\` for full node overview
|
|
313
|
+
### Workflow
|
|
385
314
|
|
|
386
|
-
|
|
315
|
+
**Start of conversation:** \`yg check\`. If errors \u2014 fix before any other work. \`yg check\` failures block commits and CI. Nothing passes until check is clean.
|
|
387
316
|
|
|
388
|
-
|
|
317
|
+
**Before touching a source file:** \`yg context --file <path>\`. Read the aspect content files listed under \`read:\`. These are the rules the reviewer will check your code against. For blast radius: \`yg impact --file <path>\`.
|
|
389
318
|
|
|
390
|
-
|
|
319
|
+
**After modifying code:** \`yg check\` \u2192 fix errors \u2192 \`yg approve --node <path>\`. Approve is part of the change \u2014 the change is not done until approve passes. Do not defer approval.
|
|
391
320
|
|
|
392
|
-
|
|
393
|
-
- **Business process with participants** \u2192 flow (\`flows/<name>/\` with \`yg-flow.yaml\`). Process-level requirements \u2192 flow aspects.
|
|
394
|
-
- **Node identity** \u2192 \`description\` field in \`yg-node.yaml\` (1-2 sentences).
|
|
395
|
-
- **Already visible in source code** \u2192 not captured in the graph.
|
|
396
|
-
- **Non-enforceable knowledge** (business strategy, personas, design decisions) \u2192 not captured in the graph. If it can be made enforceable, write it as an aspect.
|
|
321
|
+
**End of conversation:** \`yg check\` \u2014 resolve all drift. \`yg check\` failures block CI. If drift remains, the build breaks.
|
|
397
322
|
|
|
398
|
-
|
|
323
|
+
**Unmapped files:** \`yg context --file\` will say if a file has no owner and suggest candidates. Either add it to an existing node's mapping or create a new node. Code without graph coverage works but is not verified \u2014 inform the user and propose options.
|
|
399
324
|
|
|
400
|
-
|
|
401
|
-
|---|---|
|
|
402
|
-
| Cross-cutting rule (3+ nodes) | Aspect content.md |
|
|
403
|
-
| Architectural invariant for a node type | Architecture default aspect |
|
|
404
|
-
| Business process participation | Flow (\`yg-flow.yaml nodes\`) |
|
|
405
|
-
| Process-level requirement | Flow \`aspects\` + aspect directory |
|
|
406
|
-
| Node identity (brief) | \`description\` in yg-node.yaml |
|
|
407
|
-
| Already visible in source code or config files | Not captured |
|
|
408
|
-
| Non-enforceable knowledge | Not captured |
|
|
325
|
+
**Greenfield (no nodes yet):** Graph before code. Create architecture types, aspects, and nodes first \u2014 they are the specification. Then implement code that satisfies the aspects. \`yg check\` will guide you through coverage gaps.
|
|
409
326
|
|
|
410
|
-
###
|
|
327
|
+
### When to Create Graph Elements
|
|
411
328
|
|
|
412
|
-
|
|
413
|
-
- [ ] 2. Create \`aspects/<id>/\` directory
|
|
414
|
-
- [ ] 3. Write \`yg-aspect.yaml\` \u2014 name, description, optional implies
|
|
415
|
-
- [ ] 4. Write content \`.md\` files: WHAT must be satisfied + WHY (user's words, do not invent)
|
|
416
|
-
- [ ] 5. \`yg check\`
|
|
329
|
+
**Aspect** \u2014 when the same pattern appears in 3+ files AND the reviewer can verify it against source code. Both conditions. "Every handler logs audit trail" \u2014 pattern + verifiable = aspect. "Code should be readable" \u2014 not verifiable, not an aspect. Read \`schemas/yg-aspect.yaml\` before creating. Content \`.md\` files state WHAT must be satisfied and WHY \u2014 use the user's words, never invent rationale. Things that do NOT become aspects: knowledge already visible in source code (imports, config), non-enforceable knowledge (business strategy, personas, pricing), and conventions the reviewer cannot check against code.
|
|
417
330
|
|
|
418
|
-
|
|
331
|
+
**Flow** \u2014 when you see a sequence of steps toward a business goal. Not code call sequences \u2014 real-world processes. "User places an order" = flow. "Handler calls service" = relation between nodes. Read \`schemas/yg-flow.yaml\` before creating.
|
|
419
332
|
|
|
420
|
-
|
|
333
|
+
**Node** \u2014 one per cohesive feature area. Not per directory, not per file. If a node would map >10 source files or cover >3 distinct workflows, split into children. Why: the reviewer sees ALL files in a node. Too many files = reviewer loses context and produces false rejections. Aim for 2-5 source files per node with aspects. Read \`schemas/yg-node.yaml\` before creating.
|
|
421
334
|
|
|
422
|
-
|
|
423
|
-
- [ ] 2. Create \`flows/<name>/\` directory
|
|
424
|
-
- [ ] 3. Write \`yg-flow.yaml\` \u2014 name, description, nodes (participant list), and flow-level aspects
|
|
425
|
-
- [ ] 4. \`yg check\`
|
|
335
|
+
**Architecture change** \u2014 when existing types don't fit the project structure. Always confirm with the user. Never silently modify \`yg-architecture.yaml\`. If a relation between types is forbidden, present the constraint and let the user decide: use an allowed relation type, change the node type, or update the architecture.
|
|
426
336
|
|
|
427
|
-
|
|
337
|
+
**\`when\` predicate on an aspect or attach site** \u2014 when the aspect applies to
|
|
338
|
+
only a subset of nodes under a common attach channel. Prefer \`when\` over
|
|
339
|
+
splitting node types (proliferation of types). Prefer \`when\` over leaving
|
|
340
|
+
the decision to the reviewer textually inside \`content.md\`; \`when\` is
|
|
341
|
+
deterministic, has zero LLM cost, and keeps the graph as the source of
|
|
342
|
+
truth for applicability.
|
|
428
343
|
|
|
429
|
-
|
|
344
|
+
### Aspect Discovery
|
|
430
345
|
|
|
431
|
-
|
|
346
|
+
Aspects emerge from patterns \u2014 greenfield and brownfield:
|
|
432
347
|
|
|
433
|
-
|
|
348
|
+
- After working on 3+ files in the same area: are you applying the same pattern? If yes, create an aspect.
|
|
349
|
+
- Watch for "invisible" aspects: audit logging, webhook dispatch, auth guards, job dispatch \u2014 cross-cutting but easy to miss.
|
|
350
|
+
- Brownfield: same utility called in 3+ files = aspect waiting to be created.
|
|
434
351
|
|
|
435
|
-
|
|
436
|
-
ports:
|
|
437
|
-
charge:
|
|
438
|
-
description: "Charge payment"
|
|
439
|
-
aspects: [correlation-tracking]
|
|
440
|
-
\`\`\`
|
|
352
|
+
### Delegating to Subagents
|
|
441
353
|
|
|
442
|
-
|
|
354
|
+
Subagents don't inherit Yggdrasil knowledge. First instruction in every subagent prompt:
|
|
443
355
|
|
|
444
|
-
\`\`\`
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
356
|
+
\`\`\`
|
|
357
|
+
BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
|
|
358
|
+
DELIVERABLES \u2014 all required, incomplete work will be rejected:
|
|
359
|
+
1. Working source code
|
|
360
|
+
2. Graph nodes for every new/modified source file
|
|
361
|
+
3. \`yg check\` passing
|
|
449
362
|
\`\`\`
|
|
450
363
|
|
|
451
|
-
|
|
452
|
-
At approve time: Reviewer verifies consumer satisfies port-required aspects.
|
|
453
|
-
|
|
454
|
-
### CLI Commands
|
|
455
|
-
|
|
456
|
-
Core: \`yg check\`, \`yg context --node/--file\`, \`yg impact --node/--file/--aspect/--flow\`, \`yg approve --node/--aspect/--flow\`
|
|
457
|
-
Navigation: \`yg tree [--root <path>] [--depth <n>]\`, \`yg aspects\`, \`yg flows\`, \`yg owner --file\`
|
|
458
|
-
Setup: \`yg init\`
|
|
459
|
-
Debug: \`yg approve --dry-run --node <path>\` \u2014 preview reviewer prompt without LLM call
|
|
460
|
-
|
|
461
|
-
### Error Categories
|
|
462
|
-
|
|
463
|
-
CLI groups errors into categories. Each message tells you what happened, why,
|
|
464
|
-
and what command to run next.
|
|
465
|
-
|
|
466
|
-
- **Drift (\`source-drift\`, \`upstream-drift\`):** source files or upstream context changed since last approve. Run approve workflow.
|
|
467
|
-
- **Structural (\`yaml-invalid\`, \`config-invalid\`, \`relation-broken\`, etc.):** YAML broken or graph inconsistent. Fix the YAML.
|
|
468
|
-
- **Coverage (\`unmapped-files\`, \`mapping-path-missing\`):** source files not mapped. Bootstrap workflow.
|
|
469
|
-
- **Completeness (\`description-missing\`):** required fields missing. Add them.
|
|
470
|
-
- **Architecture (\`aspect-undefined\`, \`relation-target-forbidden\`, \`port-*\`, etc.):** references broken or contracts violated. Fix references.
|
|
471
|
-
- **Semantic (\`aspect-violation\`, approve only):** Reviewer found aspects not satisfied in source code.
|
|
472
|
-
|
|
473
|
-
Follow the CLI's suggested next command.
|
|
474
|
-
|
|
475
|
-
### Approve Enforcement
|
|
476
|
-
|
|
477
|
-
Approve is the architecture enforcement gate. Binary \u2014 no flags, no negotiation.
|
|
478
|
-
|
|
479
|
-
**How it works:**
|
|
480
|
-
1. Source or upstream context changed \u2192 run reviewer \u2192 reviewer checks each aspect's content.md against source code
|
|
481
|
-
2. Reviewer satisfied \u2192 \`approved\`, new baseline recorded
|
|
482
|
-
3. Reviewer not satisfied \u2192 \`refused\` with \`aspect-violation\` \u2014 fix source code and re-run
|
|
483
|
-
|
|
484
|
-
**Three modes:**
|
|
485
|
-
|
|
486
|
-
- \`yg approve --node <path> [<path2> ...]\` \u2014 one or more node paths. Multiple paths run as a batch.
|
|
487
|
-
- \`yg approve --aspect <id>\` \u2014 batch approve all cascade nodes caused by this aspect change.
|
|
488
|
-
- \`yg approve --flow <name>\` \u2014 batch approve all cascade nodes caused by this flow change.
|
|
489
|
-
|
|
490
|
-
Batch mode runs approvals in parallel (up to \`parallel\` config limit). Use batch when \`yg check\` suggests it in \`suggestedNext\`.
|
|
491
|
-
|
|
492
|
-
**Do NOT interrupt \`yg approve\`.** When reviewer is configured, approve calls the reviewer for every aspect across every source file \u2014 this takes time and is intentional. Interrupting it leaves drift state unrecorded and forces a re-run.
|
|
493
|
-
|
|
494
|
-
**Always read the FULL raw output of \`yg approve\`.** Every aspect result, every error message \u2014 read it all. The reviewer already ran and the cost is paid; the output is the return on that investment.
|
|
495
|
-
|
|
496
|
-
Always run the command without \`| grep\`, \`| head\`, \`| tail\`, or any filter that discards lines. Saving to a file and reading it (\`tee\`) is fine \u2014 that preserves all data. The rule is: all reviewer output must reach you unmodified.
|
|
497
|
-
|
|
498
|
-
Always batch at most 3-5 nodes per approve invocation. This is a maximum, not a suggestion.
|
|
499
|
-
|
|
500
|
-
**When reviewer rejects \u2014 decision tree:**
|
|
501
|
-
|
|
502
|
-
1. **Code violates aspect** \u2192 fix the code. This is the common case.
|
|
503
|
-
2. **Code is compliant but aspect wording is ambiguous** \u2192 fix the aspect content.md to be clearer. The escape hatch is improving the rule, not bypassing enforcement.
|
|
504
|
-
3. **Reviewer cannot verify because node has too many files** \u2192 the reviewer sees all source files in the node. If the node is too large, the reviewer lacks focused context. Split the node into smaller nodes so each has 2-5 source files with the relevant aspects. Never remove aspects to make approve pass \u2014 that disables enforcement.
|
|
505
|
-
4. **Aspect applies to this node but not to all its files** \u2192 the aspect may be too broad for this node. Either split the node (files that need the aspect vs. files that don't), or refine the aspect content.md to scope which files it applies to.
|
|
506
|
-
|
|
507
|
-
**Never remove an aspect from a node to bypass a rejection.** If approve fails, the fix is in the code, the aspect wording, or the node structure \u2014 not in removing enforcement.`;
|
|
508
|
-
var GUARD_RAILS = `## GUARD RAILS
|
|
509
|
-
|
|
510
|
-
### Core Rules
|
|
511
|
-
|
|
512
|
-
1. **Graph first.** Before reading, researching, planning, or modifying ANY source file, run \`yg context --file <path>\`. For blast radius, also run \`yg impact\`. The graph is your primary source of architectural understanding.
|
|
513
|
-
2. **Aspects are the specification; code implements; approve verifies.** Aspects define enforceable rules. Code must satisfy them. Approve checks compliance. This is the enforcement loop.
|
|
514
|
-
3. **Check blocks commits and CI.** \`yg check\` must pass before every commit. Treat this as fact. All errors (drift, structural, coverage, completeness) must be resolved.
|
|
515
|
-
4. **Never invent rationale.** If you don't know why a requirement exists, ask the user. Never hallucinate the reason behind an aspect.
|
|
516
|
-
5. **Ask before resolving ambiguity.** When multiple valid interpretations exist, stop, list options, ask the user. Never silently choose.
|
|
517
|
-
6. **Yggdrasil is invisible to the user.** Never mention the graph, aspects, flows, nodes, \`yg\` commands, or \`.yggdrasil/\` in conversation with the user. Present graph knowledge as your understanding \u2014 "this module handles X" not "the graph says this module handles X."
|
|
518
|
-
|
|
519
|
-
### Recognizing Graph-Required Actions
|
|
520
|
-
|
|
521
|
-
What matters is the ACTION you are performing, not what instructed it. If the action involves reading, understanding, or modifying mapped code, the graph protocol applies \u2014 whether the instruction came from a skill, a plan, a user message, a brainstorming session, a debugging workflow, or your own initiative.
|
|
522
|
-
|
|
523
|
-
**Actions that require \`yg context --file\`:**
|
|
524
|
-
|
|
525
|
-
- Reading or exploring source files to understand a component
|
|
526
|
-
- Proposing approaches, designs, or plans for changing code
|
|
527
|
-
- Reviewing or debugging code
|
|
528
|
-
- Any form of reasoning about how mapped code works or should change
|
|
529
|
-
|
|
530
|
-
**Actions that also require \`yg impact\`:**
|
|
531
|
-
|
|
532
|
-
- Assessing blast radius before changing or removing a component
|
|
533
|
-
- Finding all dependents of a component
|
|
534
|
-
- Planning cross-cutting refactors or feature removals
|
|
535
|
-
|
|
536
|
-
**Actions that do NOT require yg:**
|
|
537
|
-
|
|
538
|
-
- Git operations (log, diff, status, blame)
|
|
539
|
-
- Reading documentation, READMEs, or config files outside \`.yggdrasil/\`
|
|
540
|
-
- Running tests, builds, or linters
|
|
541
|
-
- Working with files that \`yg context --file\` reports as unmapped
|
|
542
|
-
|
|
543
|
-
### Operational Rules
|
|
544
|
-
|
|
545
|
-
- **English only** for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
546
|
-
- **Read schemas before creating** any \`yg-node.yaml\`, \`yg-aspect.yaml\`, or \`yg-flow.yaml\`.
|
|
547
|
-
- **Tools read, you write.** The \`yg\` CLI only reads, validates, and manages metadata. You create and edit files manually.
|
|
548
|
-
- **Incremental approval.** Run \`yg approve\` per node after every 3-5 source file changes. Do not defer to end of task.
|
|
549
|
-
- **Never defer approval.** When you finish modifying code, approve immediately. Do not say "I'll approve later" or leave drift for the next session. Approval is part of the change \u2014 the change is not done until approve passes.
|
|
550
|
-
- **Description maintenance.** Every \`yg-node.yaml\`, \`yg-aspect.yaml\`, and \`yg-flow.yaml\` has a \`description\` field. Write it when creating new elements. Update it when the element's identity or purpose changes.
|
|
551
|
-
|
|
552
|
-
### Aspect Discovery During Implementation
|
|
553
|
-
|
|
554
|
-
Aspects emerge from patterns \u2014 in greenfield AND brownfield:
|
|
555
|
-
|
|
556
|
-
- **After working on 3+ files in the same area, pause and check:** Are you applying the same pattern repeatedly? If YES, stop and create an aspect NOW.
|
|
557
|
-
- **Watch for "invisible" aspects:** Patterns that don't feel "architectural" but ARE cross-cutting: audit logging on every mutation, webhook dispatch after state changes, job dispatch for async operations, authorization guards on every endpoint.
|
|
558
|
-
- **Brownfield trigger:** When you read existing code and see the same utility called in 3+ files, that IS an aspect waiting to be created.
|
|
364
|
+
Code without graph updates = incomplete work.
|
|
559
365
|
|
|
560
|
-
###
|
|
366
|
+
### \`yg-suppress\` \u2014 Inline Aspect Waiver
|
|
561
367
|
|
|
562
|
-
|
|
368
|
+
Source code comments with \`yg-suppress(<aspect-path>) <reason>\` waive a specific aspect. The reviewer honors these unconditionally.
|
|
563
369
|
|
|
564
|
-
|
|
565
|
-
-
|
|
566
|
-
-
|
|
567
|
-
-
|
|
568
|
-
|
|
569
|
-
- [ ] 6. \`yg check\`, \`yg approve\` per node
|
|
570
|
-
- [ ] 7. Proceed with user's original request
|
|
370
|
+
\`\`\`
|
|
371
|
+
// yg-suppress(cqrs/single-responsibility) brownfield handler, refactor planned
|
|
372
|
+
# yg-suppress(security/input-validation) static config, no user input
|
|
373
|
+
<!-- yg-suppress(accessibility/aria-labels) generated markup, tracked in JIRA-456 -->
|
|
374
|
+
\`\`\`
|
|
571
375
|
|
|
572
|
-
|
|
376
|
+
- You may propose a suppress when you see brownfield code or known tech debt violating an aspect
|
|
377
|
+
- You MUST NEVER write a suppress without explicit user confirmation \u2014 no exceptions
|
|
378
|
+
- Provide the correct aspect-path from graph context, ask the user for the reason
|
|
379
|
+
- You do not invent reasons \u2014 the user provides or approves them
|
|
380
|
+
- The marker applies contextually to surrounding code (function, class, block). At file level, it applies to the entire file.
|
|
573
381
|
|
|
574
382
|
### Escape Hatch
|
|
575
383
|
|
|
576
|
-
If the user explicitly requests a code-only change
|
|
384
|
+
If the user explicitly requests a code-only change without graph updates: comply, but warn that this creates drift. \`yg check\` will catch it \u2014 and CI will block until it's resolved. Do not run \`yg approve\` \u2014 leave the drift visible.
|
|
577
385
|
|
|
578
|
-
|
|
579
|
-
- Do NOT run \`yg approve\` \u2014 leave the drift visible.
|
|
386
|
+
### Operational Notes
|
|
580
387
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
388
|
+
- English only for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
389
|
+
- Read the relevant schema from \`schemas/\` before creating any YAML file.
|
|
390
|
+
- Every \`yg-node.yaml\`, \`yg-aspect.yaml\`, and \`yg-flow.yaml\` needs a \`description\`. Write it when creating, update it when purpose changes.
|
|
391
|
+
- When renaming or splitting a node: run \`yg flows\` and update any flow \`nodes\` lists that reference the old path. \`yg check\` will catch broken references but it's faster to fix them proactively.
|
|
392
|
+
- When unsure about anything: ask the user. Do not guess. Do not assume.
|
|
393
|
+
- Never invent rationale for aspects. If you don't know why a requirement exists, ask.`;
|
|
394
|
+
var AGENT_RULES_CONTENT = [SYSTEM, DECISIONS].join("\n\n---\n\n") + "\n";
|
|
587
395
|
|
|
588
396
|
// src/templates/platform.ts
|
|
589
397
|
var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
|
|
@@ -1363,11 +1171,31 @@ var MIGRATIONS = [
|
|
|
1363
1171
|
}
|
|
1364
1172
|
];
|
|
1365
1173
|
|
|
1174
|
+
// src/formatters/message-builder.ts
|
|
1175
|
+
function buildIssueMessage(msg) {
|
|
1176
|
+
return `${msg.what}
|
|
1177
|
+
${msg.why}
|
|
1178
|
+
${msg.next}`;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1366
1181
|
// src/cli/init.ts
|
|
1182
|
+
function getPackageRoot() {
|
|
1183
|
+
let dir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1184
|
+
while (dir !== path5.dirname(dir)) {
|
|
1185
|
+
if (existsSync(path5.join(dir, "package.json"))) {
|
|
1186
|
+
return dir;
|
|
1187
|
+
}
|
|
1188
|
+
dir = path5.dirname(dir);
|
|
1189
|
+
}
|
|
1190
|
+
throw new Error("Could not locate package root (no package.json found walking up from init module).");
|
|
1191
|
+
}
|
|
1367
1192
|
function getGraphSchemasDir() {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1193
|
+
return path5.join(getPackageRoot(), "graph-schemas");
|
|
1194
|
+
}
|
|
1195
|
+
async function getCliVersion() {
|
|
1196
|
+
const pkgPath = path5.join(getPackageRoot(), "package.json");
|
|
1197
|
+
const pkg2 = JSON.parse(await readFile4(pkgPath, "utf-8"));
|
|
1198
|
+
return pkg2.version;
|
|
1371
1199
|
}
|
|
1372
1200
|
async function refreshSchemas(yggRoot) {
|
|
1373
1201
|
const schemasDir = path5.join(yggRoot, "schemas");
|
|
@@ -1661,6 +1489,25 @@ async function createYggdrasilStructure(projectRoot, yggRoot, platform) {
|
|
|
1661
1489
|
await writeFile4(path5.join(yggRoot, ".gitignore"), GITIGNORE_CONTENT, "utf-8");
|
|
1662
1490
|
await installRulesForPlatform(projectRoot, platform);
|
|
1663
1491
|
}
|
|
1492
|
+
async function runVersionUpgrade(projectRoot, yggRoot, fromVersion, toVersion, platform) {
|
|
1493
|
+
const migrationResults = await runMigrations(fromVersion, MIGRATIONS, yggRoot);
|
|
1494
|
+
await updateConfigVersion(yggRoot, toVersion);
|
|
1495
|
+
await refreshSchemas(yggRoot);
|
|
1496
|
+
const architecturePath = path5.join(yggRoot, "yg-architecture.yaml");
|
|
1497
|
+
try {
|
|
1498
|
+
await stat2(architecturePath);
|
|
1499
|
+
} catch {
|
|
1500
|
+
await writeFile4(architecturePath, DEFAULT_ARCHITECTURE, "utf-8");
|
|
1501
|
+
}
|
|
1502
|
+
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
1503
|
+
const migrationActions = [];
|
|
1504
|
+
const migrationWarnings = [];
|
|
1505
|
+
for (const r of migrationResults) {
|
|
1506
|
+
migrationActions.push(...r.actions);
|
|
1507
|
+
migrationWarnings.push(...r.warnings);
|
|
1508
|
+
}
|
|
1509
|
+
return { rulesPath, migrationActions, migrationWarnings };
|
|
1510
|
+
}
|
|
1664
1511
|
async function existingInit(projectRoot) {
|
|
1665
1512
|
const yggRoot = path5.join(projectRoot, ".yggdrasil");
|
|
1666
1513
|
if (!isTTY()) {
|
|
@@ -1669,35 +1516,30 @@ async function existingInit(projectRoot) {
|
|
|
1669
1516
|
}
|
|
1670
1517
|
p.intro(chalk.bold("Yggdrasil Configuration"));
|
|
1671
1518
|
const currentVersion = await detectVersion(yggRoot);
|
|
1672
|
-
const cliVersion =
|
|
1519
|
+
const cliVersion = await getCliVersion();
|
|
1673
1520
|
if (currentVersion && currentVersion !== cliVersion) {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
p.log.info("2. Run yg check to verify graph integrity");
|
|
1697
|
-
p.log.info("3. Run yg approve on all nodes to establish baselines");
|
|
1698
|
-
p.outro(chalk.green(`Migrated from ${currentVersion} to ${cliVersion}.`));
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1521
|
+
p.log.step(`Graph version ${currentVersion} detected \u2014 CLI is ${cliVersion}. Upgrade required.`);
|
|
1522
|
+
p.log.info("Select the agent platform so rules and schemas advance together.");
|
|
1523
|
+
const platform = await promptPlatform();
|
|
1524
|
+
const s = p.spinner();
|
|
1525
|
+
s.start("Running migrations and installing rules...");
|
|
1526
|
+
const result = await runVersionUpgrade(projectRoot, yggRoot, currentVersion, cliVersion, platform);
|
|
1527
|
+
s.stop("Upgrade complete.");
|
|
1528
|
+
for (const action2 of result.migrationActions) {
|
|
1529
|
+
p.log.info(action2);
|
|
1530
|
+
}
|
|
1531
|
+
for (const warning of result.migrationWarnings) {
|
|
1532
|
+
p.log.warning(warning);
|
|
1533
|
+
}
|
|
1534
|
+
p.log.step("Next steps:");
|
|
1535
|
+
p.log.info("1. Run yg check to verify graph integrity");
|
|
1536
|
+
p.log.info("2. Run yg approve on all nodes to establish baselines");
|
|
1537
|
+
p.outro(
|
|
1538
|
+
chalk.green(
|
|
1539
|
+
`Migrated from ${currentVersion} to ${cliVersion}. Rules installed: ${path5.relative(projectRoot, result.rulesPath)}`
|
|
1540
|
+
)
|
|
1541
|
+
);
|
|
1542
|
+
return;
|
|
1701
1543
|
}
|
|
1702
1544
|
const action = await p.select({
|
|
1703
1545
|
message: "What would you like to do?",
|
|
@@ -1711,15 +1553,9 @@ async function existingInit(projectRoot) {
|
|
|
1711
1553
|
switch (action) {
|
|
1712
1554
|
case "upgrade": {
|
|
1713
1555
|
const platform = await promptPlatform();
|
|
1714
|
-
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
await stat2(architecturePath);
|
|
1718
|
-
} catch {
|
|
1719
|
-
await writeFile4(architecturePath, DEFAULT_ARCHITECTURE, "utf-8");
|
|
1720
|
-
}
|
|
1721
|
-
const rulesPath = await installRulesForPlatform(projectRoot, platform);
|
|
1722
|
-
p.outro(chalk.green(`Rules and schemas refreshed: ${path5.relative(projectRoot, rulesPath)}`));
|
|
1556
|
+
const fromVersion = currentVersion ?? cliVersion;
|
|
1557
|
+
const result = await runVersionUpgrade(projectRoot, yggRoot, fromVersion, cliVersion, platform);
|
|
1558
|
+
p.outro(chalk.green(`Rules and schemas refreshed: ${path5.relative(projectRoot, result.rulesPath)}`));
|
|
1723
1559
|
break;
|
|
1724
1560
|
}
|
|
1725
1561
|
case "reviewer": {
|
|
@@ -1760,16 +1596,27 @@ function registerInitCommand(program2) {
|
|
|
1760
1596
|
process.stderr.write(chalk.red("Error: No .yggdrasil/ directory found. Run 'yg init' first.\n"));
|
|
1761
1597
|
process.exit(1);
|
|
1762
1598
|
}
|
|
1763
|
-
await
|
|
1764
|
-
const
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1599
|
+
const toVersion = await getCliVersion();
|
|
1600
|
+
const fromVersion = await detectVersion(yggRoot);
|
|
1601
|
+
if (fromVersion === null) {
|
|
1602
|
+
process.stderr.write(chalk.red(buildIssueMessage({
|
|
1603
|
+
what: "No graph version detected.",
|
|
1604
|
+
why: ".yggdrasil/yg-config.yaml is missing a 'version:' field, so --upgrade cannot determine which migrations to run.",
|
|
1605
|
+
next: "Run 'yg init' interactively once to record the current version, then retry 'yg init --upgrade --platform <name>'."
|
|
1606
|
+
}) + "\n"));
|
|
1607
|
+
process.exit(1);
|
|
1769
1608
|
}
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
1609
|
+
const result = await runVersionUpgrade(
|
|
1610
|
+
projectRoot,
|
|
1611
|
+
yggRoot,
|
|
1612
|
+
fromVersion,
|
|
1613
|
+
toVersion,
|
|
1614
|
+
options.platform
|
|
1615
|
+
);
|
|
1616
|
+
process.stdout.write(
|
|
1617
|
+
`Rules and schemas refreshed: ${path5.relative(projectRoot, result.rulesPath)}
|
|
1618
|
+
`
|
|
1619
|
+
);
|
|
1773
1620
|
return;
|
|
1774
1621
|
}
|
|
1775
1622
|
let exists = false;
|
|
@@ -1926,7 +1773,219 @@ function normalizeProviderConfig(providerName, pc, generalConfig, filename) {
|
|
|
1926
1773
|
// src/io/node-parser.ts
|
|
1927
1774
|
import { readFile as readFile6 } from "fs/promises";
|
|
1928
1775
|
import { parse as parseYaml4 } from "yaml";
|
|
1929
|
-
|
|
1776
|
+
|
|
1777
|
+
// src/io/when-parser.ts
|
|
1778
|
+
var RELATION_TYPES = /* @__PURE__ */ new Set([
|
|
1779
|
+
"calls",
|
|
1780
|
+
"uses",
|
|
1781
|
+
"extends",
|
|
1782
|
+
"implements",
|
|
1783
|
+
"emits",
|
|
1784
|
+
"listens"
|
|
1785
|
+
]);
|
|
1786
|
+
var ATOMIC_KEYS = /* @__PURE__ */ new Set(["relations", "descendants", "node"]);
|
|
1787
|
+
var BOOLEAN_KEYS = /* @__PURE__ */ new Set(["all_of", "any_of", "not"]);
|
|
1788
|
+
function parseWhen(raw, ctx) {
|
|
1789
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1790
|
+
throw new Error(`${ctx}: when must be a YAML mapping`);
|
|
1791
|
+
}
|
|
1792
|
+
const keys = Object.keys(raw);
|
|
1793
|
+
if (keys.length === 0) {
|
|
1794
|
+
throw new Error(`${ctx}: when mapping must not be empty`);
|
|
1795
|
+
}
|
|
1796
|
+
const booleanKeys = keys.filter((k) => BOOLEAN_KEYS.has(k));
|
|
1797
|
+
const atomicKeys = keys.filter((k) => ATOMIC_KEYS.has(k));
|
|
1798
|
+
const unknownKeys = keys.filter((k) => !BOOLEAN_KEYS.has(k) && !ATOMIC_KEYS.has(k));
|
|
1799
|
+
if (unknownKeys.length > 0) {
|
|
1800
|
+
throw new Error(`${ctx}: unknown when operator '${unknownKeys[0]}' (expected one of: all_of, any_of, not, relations, descendants, node)`);
|
|
1801
|
+
}
|
|
1802
|
+
if (booleanKeys.length > 0 && atomicKeys.length > 0) {
|
|
1803
|
+
throw new Error(`${ctx}: when cannot mix boolean operators with atomic clauses at the same level`);
|
|
1804
|
+
}
|
|
1805
|
+
if (booleanKeys.length > 1) {
|
|
1806
|
+
throw new Error(`${ctx}: when can have at most one boolean operator at a level (got: ${booleanKeys.join(", ")})`);
|
|
1807
|
+
}
|
|
1808
|
+
if (booleanKeys.length === 1) {
|
|
1809
|
+
return parseBoolean(raw, booleanKeys[0], ctx);
|
|
1810
|
+
}
|
|
1811
|
+
return parseAtomic(raw, ctx);
|
|
1812
|
+
}
|
|
1813
|
+
function parseBoolean(raw, key, ctx) {
|
|
1814
|
+
const val = raw[key];
|
|
1815
|
+
if (key === "not") {
|
|
1816
|
+
return { not: parseWhen(val, `${ctx}/not`) };
|
|
1817
|
+
}
|
|
1818
|
+
if (!Array.isArray(val)) {
|
|
1819
|
+
throw new Error(`${ctx}: '${key}' must be an array`);
|
|
1820
|
+
}
|
|
1821
|
+
if (val.length === 0) {
|
|
1822
|
+
throw new Error(`${ctx}: '${key}' array must not be empty`);
|
|
1823
|
+
}
|
|
1824
|
+
const items = val.map((v, i) => parseWhen(v, `${ctx}/${key}[${i}]`));
|
|
1825
|
+
return key === "all_of" ? { all_of: items } : { any_of: items };
|
|
1826
|
+
}
|
|
1827
|
+
function parseAtomic(raw, ctx) {
|
|
1828
|
+
const result = {};
|
|
1829
|
+
if ("relations" in raw) {
|
|
1830
|
+
result.relations = parseRelationClause(raw.relations, `${ctx}/relations`);
|
|
1831
|
+
}
|
|
1832
|
+
if ("descendants" in raw) {
|
|
1833
|
+
result.descendants = parseDescendantsClause(raw.descendants, `${ctx}/descendants`);
|
|
1834
|
+
}
|
|
1835
|
+
if ("node" in raw) {
|
|
1836
|
+
result.node = parseNodeClause(raw.node, `${ctx}/node`);
|
|
1837
|
+
}
|
|
1838
|
+
return result;
|
|
1839
|
+
}
|
|
1840
|
+
function parseRelationClause(raw, ctx) {
|
|
1841
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1842
|
+
throw new Error(`${ctx}: relations must be a YAML mapping keyed by relation type`);
|
|
1843
|
+
}
|
|
1844
|
+
const entries = Object.entries(raw);
|
|
1845
|
+
if (entries.length === 0) {
|
|
1846
|
+
throw new Error(`${ctx}: relations mapping must not be empty`);
|
|
1847
|
+
}
|
|
1848
|
+
const out = {};
|
|
1849
|
+
for (const [relType, match] of entries) {
|
|
1850
|
+
if (!RELATION_TYPES.has(relType)) {
|
|
1851
|
+
throw new Error(`${ctx}: unknown relation type '${relType}' (valid: ${Array.from(RELATION_TYPES).join(", ")})`);
|
|
1852
|
+
}
|
|
1853
|
+
out[relType] = parseRelationMatch(match, `${ctx}/${relType}`);
|
|
1854
|
+
}
|
|
1855
|
+
return out;
|
|
1856
|
+
}
|
|
1857
|
+
function parseRelationMatch(raw, ctx) {
|
|
1858
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1859
|
+
throw new Error(`${ctx}: must be a YAML mapping`);
|
|
1860
|
+
}
|
|
1861
|
+
const obj = raw;
|
|
1862
|
+
const allowed = /* @__PURE__ */ new Set(["target_type", "target", "consumes_port"]);
|
|
1863
|
+
for (const k of Object.keys(obj)) {
|
|
1864
|
+
if (!allowed.has(k)) {
|
|
1865
|
+
throw new Error(`${ctx}: unknown field '${k}' (allowed: target_type, target, consumes_port)`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const out = {};
|
|
1869
|
+
if ("target_type" in obj) {
|
|
1870
|
+
if (typeof obj.target_type !== "string" || obj.target_type.trim() === "") {
|
|
1871
|
+
throw new Error(`${ctx}: target_type must be a non-empty string`);
|
|
1872
|
+
}
|
|
1873
|
+
out.target_type = obj.target_type.trim();
|
|
1874
|
+
}
|
|
1875
|
+
if ("target" in obj) {
|
|
1876
|
+
if (typeof obj.target !== "string" || obj.target.trim() === "") {
|
|
1877
|
+
throw new Error(`${ctx}: target must be a non-empty string (node path relative to model/)`);
|
|
1878
|
+
}
|
|
1879
|
+
out.target = obj.target.trim();
|
|
1880
|
+
}
|
|
1881
|
+
if ("consumes_port" in obj) {
|
|
1882
|
+
if (typeof obj.consumes_port !== "string" || obj.consumes_port.trim() === "") {
|
|
1883
|
+
throw new Error(`${ctx}: consumes_port must be a non-empty string`);
|
|
1884
|
+
}
|
|
1885
|
+
out.consumes_port = obj.consumes_port.trim();
|
|
1886
|
+
}
|
|
1887
|
+
if (Object.keys(out).length === 0) {
|
|
1888
|
+
throw new Error(`${ctx}: at least one of target_type, target, consumes_port must be present`);
|
|
1889
|
+
}
|
|
1890
|
+
return out;
|
|
1891
|
+
}
|
|
1892
|
+
function parseDescendantsClause(raw, ctx) {
|
|
1893
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1894
|
+
throw new Error(`${ctx}: descendants must be a YAML mapping`);
|
|
1895
|
+
}
|
|
1896
|
+
const obj = raw;
|
|
1897
|
+
const allowed = /* @__PURE__ */ new Set(["relations", "type", "has_port"]);
|
|
1898
|
+
for (const k of Object.keys(obj)) {
|
|
1899
|
+
if (!allowed.has(k)) {
|
|
1900
|
+
throw new Error(`${ctx}: unknown field '${k}' (allowed: relations, type, has_port)`);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const out = {};
|
|
1904
|
+
if ("relations" in obj) {
|
|
1905
|
+
out.relations = parseRelationClause(obj.relations, `${ctx}/relations`);
|
|
1906
|
+
}
|
|
1907
|
+
if ("type" in obj) {
|
|
1908
|
+
if (typeof obj.type !== "string" || obj.type.trim() === "") {
|
|
1909
|
+
throw new Error(`${ctx}: type must be a non-empty string`);
|
|
1910
|
+
}
|
|
1911
|
+
out.type = obj.type.trim();
|
|
1912
|
+
}
|
|
1913
|
+
if ("has_port" in obj) {
|
|
1914
|
+
if (typeof obj.has_port !== "string" || obj.has_port.trim() === "") {
|
|
1915
|
+
throw new Error(`${ctx}: has_port must be a non-empty string`);
|
|
1916
|
+
}
|
|
1917
|
+
out.has_port = obj.has_port.trim();
|
|
1918
|
+
}
|
|
1919
|
+
if (Object.keys(out).length === 0) {
|
|
1920
|
+
throw new Error(`${ctx}: at least one of relations, type, has_port must be present`);
|
|
1921
|
+
}
|
|
1922
|
+
return out;
|
|
1923
|
+
}
|
|
1924
|
+
function parseNodeClause(raw, ctx) {
|
|
1925
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
1926
|
+
throw new Error(`${ctx}: node must be a YAML mapping`);
|
|
1927
|
+
}
|
|
1928
|
+
const obj = raw;
|
|
1929
|
+
const allowed = /* @__PURE__ */ new Set(["type", "has_port", "has_mapping"]);
|
|
1930
|
+
for (const k of Object.keys(obj)) {
|
|
1931
|
+
if (!allowed.has(k)) {
|
|
1932
|
+
throw new Error(`${ctx}: unknown field '${k}' (allowed: type, has_port, has_mapping)`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const out = {};
|
|
1936
|
+
if ("type" in obj) {
|
|
1937
|
+
if (typeof obj.type !== "string" || obj.type.trim() === "") {
|
|
1938
|
+
throw new Error(`${ctx}: type must be a non-empty string`);
|
|
1939
|
+
}
|
|
1940
|
+
out.type = obj.type.trim();
|
|
1941
|
+
}
|
|
1942
|
+
if ("has_port" in obj) {
|
|
1943
|
+
if (typeof obj.has_port !== "string" || obj.has_port.trim() === "") {
|
|
1944
|
+
throw new Error(`${ctx}: has_port must be a non-empty string`);
|
|
1945
|
+
}
|
|
1946
|
+
out.has_port = obj.has_port.trim();
|
|
1947
|
+
}
|
|
1948
|
+
if ("has_mapping" in obj) {
|
|
1949
|
+
if (typeof obj.has_mapping !== "boolean") {
|
|
1950
|
+
throw new Error(`${ctx}: has_mapping must be a boolean`);
|
|
1951
|
+
}
|
|
1952
|
+
out.has_mapping = obj.has_mapping;
|
|
1953
|
+
}
|
|
1954
|
+
if (Object.keys(out).length === 0) {
|
|
1955
|
+
throw new Error(`${ctx}: at least one of type, has_port, has_mapping must be present`);
|
|
1956
|
+
}
|
|
1957
|
+
return out;
|
|
1958
|
+
}
|
|
1959
|
+
function parseAspectAttachment(raw, ctx) {
|
|
1960
|
+
if (typeof raw === "string") {
|
|
1961
|
+
const id = raw.trim();
|
|
1962
|
+
if (id === "") {
|
|
1963
|
+
throw new Error(`${ctx}: aspect id must be a non-empty string`);
|
|
1964
|
+
}
|
|
1965
|
+
return { id };
|
|
1966
|
+
}
|
|
1967
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
1968
|
+
const obj = raw;
|
|
1969
|
+
if (typeof obj.id !== "string" || obj.id.trim() === "") {
|
|
1970
|
+
throw new Error(`${ctx}: object form requires 'id' as a non-empty string`);
|
|
1971
|
+
}
|
|
1972
|
+
const result = { id: obj.id.trim() };
|
|
1973
|
+
const allowed = /* @__PURE__ */ new Set(["id", "when"]);
|
|
1974
|
+
for (const k of Object.keys(obj)) {
|
|
1975
|
+
if (!allowed.has(k)) {
|
|
1976
|
+
throw new Error(`${ctx}: unknown field '${k}' in aspect attachment (allowed: id, when)`);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if ("when" in obj) {
|
|
1980
|
+
result.when = parseWhen(obj.when, `${ctx}/when`);
|
|
1981
|
+
}
|
|
1982
|
+
return result;
|
|
1983
|
+
}
|
|
1984
|
+
throw new Error(`${ctx}: aspect attachment must be a string or an object with 'id' (and optional 'when')`);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/io/node-parser.ts
|
|
1988
|
+
var RELATION_TYPES2 = [
|
|
1930
1989
|
"uses",
|
|
1931
1990
|
"calls",
|
|
1932
1991
|
"extends",
|
|
@@ -1935,7 +1994,7 @@ var RELATION_TYPES = [
|
|
|
1935
1994
|
"listens"
|
|
1936
1995
|
];
|
|
1937
1996
|
function isValidRelationType(t) {
|
|
1938
|
-
return typeof t === "string" &&
|
|
1997
|
+
return typeof t === "string" && RELATION_TYPES2.includes(t);
|
|
1939
1998
|
}
|
|
1940
1999
|
async function parseNodeYaml(filePath) {
|
|
1941
2000
|
const content = await readFile6(filePath, "utf-8");
|
|
@@ -1952,52 +2011,43 @@ async function parseNodeYaml(filePath) {
|
|
|
1952
2011
|
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
1953
2012
|
const relations = parseRelations(raw.relations, filePath);
|
|
1954
2013
|
const mapping = parseMapping(raw.mapping, filePath);
|
|
1955
|
-
const
|
|
2014
|
+
const aspectsResult = parseAspects(raw.aspects, filePath);
|
|
1956
2015
|
const ports = parsePorts(raw.ports, filePath);
|
|
1957
2016
|
return {
|
|
1958
2017
|
name: raw.name.trim(),
|
|
1959
2018
|
type: raw.type.trim(),
|
|
1960
2019
|
description,
|
|
1961
|
-
aspects,
|
|
2020
|
+
aspects: aspectsResult.aspects,
|
|
2021
|
+
...aspectsResult.aspectWhens && { aspectWhens: aspectsResult.aspectWhens },
|
|
1962
2022
|
relations: relations.length > 0 ? relations : void 0,
|
|
1963
2023
|
mapping,
|
|
1964
2024
|
ports
|
|
1965
2025
|
};
|
|
1966
2026
|
}
|
|
1967
2027
|
function parseAspects(raw, filePath) {
|
|
1968
|
-
if (raw === void 0 || raw === null) return
|
|
2028
|
+
if (raw === void 0 || raw === null) return {};
|
|
1969
2029
|
if (!Array.isArray(raw)) {
|
|
1970
2030
|
throw new Error(`yg-node.yaml at ${filePath}: 'aspects' must be an array`);
|
|
1971
2031
|
}
|
|
1972
|
-
if (raw.length === 0) return
|
|
1973
|
-
const
|
|
1974
|
-
|
|
2032
|
+
if (raw.length === 0) return {};
|
|
2033
|
+
const aspects = [];
|
|
2034
|
+
let aspectWhens;
|
|
2035
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1975
2036
|
for (let i = 0; i < raw.length; i++) {
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
`yg-node.yaml at ${filePath}: aspects[${i}] must be a non-empty string`
|
|
1983
|
-
);
|
|
1984
|
-
}
|
|
1985
|
-
} else if (typeof item === "object" && item !== null) {
|
|
1986
|
-
throw new Error(
|
|
1987
|
-
`yg-node.yaml at ${filePath}: aspects must be an array of strings.`
|
|
1988
|
-
);
|
|
1989
|
-
} else {
|
|
1990
|
-
throw new Error(`yg-node.yaml at ${filePath}: aspects[${i}] must be a string`);
|
|
2037
|
+
const parsed = parseAspectAttachment(
|
|
2038
|
+
raw[i],
|
|
2039
|
+
`yg-node.yaml at ${filePath}: aspects[${i}]`
|
|
2040
|
+
);
|
|
2041
|
+
if (seen.has(parsed.id)) {
|
|
2042
|
+
throw new Error(`yg-node.yaml at ${filePath}: duplicate aspect '${parsed.id}' in aspects list`);
|
|
1991
2043
|
}
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
);
|
|
2044
|
+
seen.add(parsed.id);
|
|
2045
|
+
aspects.push(parsed.id);
|
|
2046
|
+
if (parsed.when) {
|
|
2047
|
+
(aspectWhens ??= {})[parsed.id] = parsed.when;
|
|
1996
2048
|
}
|
|
1997
|
-
seenAspects.add(aspectId);
|
|
1998
|
-
result.push(aspectId);
|
|
1999
2049
|
}
|
|
2000
|
-
return
|
|
2050
|
+
return { aspects: aspects.length > 0 ? aspects : void 0, aspectWhens };
|
|
2001
2051
|
}
|
|
2002
2052
|
function parseRelations(raw, filePath) {
|
|
2003
2053
|
if (raw === void 0) return [];
|
|
@@ -2089,13 +2139,28 @@ function parsePorts(rawPorts, filePath) {
|
|
|
2089
2139
|
if (!Array.isArray(obj.aspects)) {
|
|
2090
2140
|
throw new Error(`yg-node.yaml at ${filePath}: ports.${name}.aspects must be an array`);
|
|
2091
2141
|
}
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
2142
|
+
const portAspects = [];
|
|
2143
|
+
let portAspectWhens;
|
|
2144
|
+
const seenPortAspects = /* @__PURE__ */ new Set();
|
|
2145
|
+
for (let i = 0; i < obj.aspects.length; i++) {
|
|
2146
|
+
const parsed = parseAspectAttachment(
|
|
2147
|
+
obj.aspects[i],
|
|
2148
|
+
`yg-node.yaml at ${filePath}: ports.${name}.aspects[${i}]`
|
|
2149
|
+
);
|
|
2150
|
+
if (seenPortAspects.has(parsed.id)) {
|
|
2151
|
+
throw new Error(`yg-node.yaml at ${filePath}: ports.${name}.aspects has duplicate '${parsed.id}'`);
|
|
2095
2152
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2153
|
+
seenPortAspects.add(parsed.id);
|
|
2154
|
+
portAspects.push(parsed.id);
|
|
2155
|
+
if (parsed.when) {
|
|
2156
|
+
(portAspectWhens ??= {})[parsed.id] = parsed.when;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
ports[name] = {
|
|
2160
|
+
description: obj.description.trim(),
|
|
2161
|
+
aspects: portAspects,
|
|
2162
|
+
...portAspectWhens && { aspectWhens: portAspectWhens }
|
|
2163
|
+
};
|
|
2099
2164
|
}
|
|
2100
2165
|
return Object.keys(ports).length > 0 ? ports : void 0;
|
|
2101
2166
|
}
|
|
@@ -2146,17 +2211,34 @@ async function parseAspect(aspectDir, aspectYamlPath, id) {
|
|
|
2146
2211
|
const description = typeof raw.description === "string" ? raw.description.trim() : void 0;
|
|
2147
2212
|
const artifacts = await readArtifacts(aspectDir, ["yg-aspect.yaml"]);
|
|
2148
2213
|
let implies;
|
|
2214
|
+
let impliesWhens;
|
|
2149
2215
|
if (raw.implies !== void 0) {
|
|
2150
2216
|
if (!Array.isArray(raw.implies)) {
|
|
2151
|
-
throw new Error(`yg-aspect.yaml at ${aspectYamlPath}: 'implies' must be an array
|
|
2217
|
+
throw new Error(`yg-aspect.yaml at ${aspectYamlPath}: 'implies' must be an array`);
|
|
2152
2218
|
}
|
|
2153
|
-
implies =
|
|
2219
|
+
implies = [];
|
|
2220
|
+
for (let i = 0; i < raw.implies.length; i++) {
|
|
2221
|
+
const parsed = parseAspectAttachment(
|
|
2222
|
+
raw.implies[i],
|
|
2223
|
+
`yg-aspect.yaml at ${aspectYamlPath}: implies[${i}]`
|
|
2224
|
+
);
|
|
2225
|
+
implies.push(parsed.id);
|
|
2226
|
+
if (parsed.when) {
|
|
2227
|
+
(impliesWhens ??= {})[parsed.id] = parsed.when;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
let when;
|
|
2232
|
+
if (raw.when !== void 0) {
|
|
2233
|
+
when = parseWhen(raw.when, `yg-aspect.yaml at ${aspectYamlPath}: when`);
|
|
2154
2234
|
}
|
|
2155
2235
|
return {
|
|
2156
2236
|
name: raw.name.trim(),
|
|
2157
2237
|
id: idTrimmed,
|
|
2158
2238
|
description,
|
|
2159
2239
|
implies,
|
|
2240
|
+
...impliesWhens && { impliesWhens },
|
|
2241
|
+
...when && { when },
|
|
2160
2242
|
artifacts
|
|
2161
2243
|
};
|
|
2162
2244
|
}
|
|
@@ -2188,19 +2270,30 @@ async function parseFlow(flowDir, flowYamlPath) {
|
|
|
2188
2270
|
);
|
|
2189
2271
|
}
|
|
2190
2272
|
let aspects;
|
|
2273
|
+
let aspectWhens;
|
|
2191
2274
|
if (raw.aspects !== void 0) {
|
|
2192
2275
|
if (!Array.isArray(raw.aspects)) {
|
|
2193
2276
|
throw new Error(`yg-flow.yaml at ${flowYamlPath}: 'aspects' must be an array of strings`);
|
|
2194
2277
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2278
|
+
aspects = [];
|
|
2279
|
+
for (let i = 0; i < raw.aspects.length; i++) {
|
|
2280
|
+
const parsed = parseAspectAttachment(
|
|
2281
|
+
raw.aspects[i],
|
|
2282
|
+
`yg-flow.yaml at ${flowYamlPath}: aspects[${i}]`
|
|
2283
|
+
);
|
|
2284
|
+
aspects.push(parsed.id);
|
|
2285
|
+
if (parsed.when) {
|
|
2286
|
+
(aspectWhens ??= {})[parsed.id] = parsed.when;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2197
2289
|
}
|
|
2198
2290
|
return {
|
|
2199
2291
|
path: path8.basename(flowDir),
|
|
2200
2292
|
name: raw.name.trim(),
|
|
2201
2293
|
description,
|
|
2202
2294
|
nodes: nodePaths,
|
|
2203
|
-
...aspects !== void 0 && { aspects }
|
|
2295
|
+
...aspects !== void 0 && { aspects },
|
|
2296
|
+
...aspectWhens && { aspectWhens }
|
|
2204
2297
|
};
|
|
2205
2298
|
}
|
|
2206
2299
|
|
|
@@ -2247,12 +2340,28 @@ async function parseArchitecture(filePath) {
|
|
|
2247
2340
|
`yg-architecture.yaml: node type '${typeName}' has unknown field 'integration_aspects'. Use ports on the target node instead.`
|
|
2248
2341
|
);
|
|
2249
2342
|
}
|
|
2250
|
-
|
|
2343
|
+
let aspects;
|
|
2344
|
+
let aspectWhens;
|
|
2345
|
+
if (Array.isArray(entry.aspects)) {
|
|
2346
|
+
aspects = [];
|
|
2347
|
+
for (let i = 0; i < entry.aspects.length; i++) {
|
|
2348
|
+
const parsed = parseAspectAttachment(
|
|
2349
|
+
entry.aspects[i],
|
|
2350
|
+
`yg-architecture.yaml: node_types.${typeName}.aspects[${i}]`
|
|
2351
|
+
);
|
|
2352
|
+
aspects.push(parsed.id);
|
|
2353
|
+
if (parsed.when) {
|
|
2354
|
+
(aspectWhens ??= {})[parsed.id] = parsed.when;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
if (aspects.length === 0) aspects = void 0;
|
|
2358
|
+
}
|
|
2251
2359
|
const parents = Array.isArray(entry.parents) ? entry.parents.filter((t) => typeof t === "string") : void 0;
|
|
2252
2360
|
const relations = parseRelations2(entry.relations, typeName);
|
|
2253
2361
|
nodeTypes[typeName] = {
|
|
2254
2362
|
description: entry.description,
|
|
2255
|
-
aspects
|
|
2363
|
+
aspects,
|
|
2364
|
+
...aspectWhens && { aspectWhens },
|
|
2256
2365
|
parents: parents && parents.length > 0 ? parents : void 0,
|
|
2257
2366
|
relations
|
|
2258
2367
|
};
|
|
@@ -2529,57 +2638,191 @@ async function loadSchemas(schemasDir) {
|
|
|
2529
2638
|
import { readFile as readFile13 } from "fs/promises";
|
|
2530
2639
|
import path12 from "path";
|
|
2531
2640
|
|
|
2641
|
+
// src/core/when-evaluator.ts
|
|
2642
|
+
function evaluateWhen(predicate, node, graph) {
|
|
2643
|
+
if ("all_of" in predicate) {
|
|
2644
|
+
return predicate.all_of.every((p2) => evaluateWhen(p2, node, graph));
|
|
2645
|
+
}
|
|
2646
|
+
if ("any_of" in predicate) {
|
|
2647
|
+
return predicate.any_of.some((p2) => evaluateWhen(p2, node, graph));
|
|
2648
|
+
}
|
|
2649
|
+
if ("not" in predicate) {
|
|
2650
|
+
return !evaluateWhen(predicate.not, node, graph);
|
|
2651
|
+
}
|
|
2652
|
+
return evaluateAtomic(predicate, node, graph);
|
|
2653
|
+
}
|
|
2654
|
+
function evaluateAtomic(clause, node, graph) {
|
|
2655
|
+
if (clause.relations) {
|
|
2656
|
+
if (!evaluateRelationClause(clause.relations, node.meta.relations ?? [], graph)) return false;
|
|
2657
|
+
}
|
|
2658
|
+
if (clause.descendants) {
|
|
2659
|
+
if (!evaluateDescendantsClause(clause.descendants, node, graph)) return false;
|
|
2660
|
+
}
|
|
2661
|
+
if (clause.node) {
|
|
2662
|
+
if (!evaluateNodeClause(clause.node, node)) return false;
|
|
2663
|
+
}
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
function evaluateRelationClause(rc, relations, graph) {
|
|
2667
|
+
for (const [relType, match] of Object.entries(rc)) {
|
|
2668
|
+
if (!match) continue;
|
|
2669
|
+
const candidates = relations.filter((r) => r.type === relType);
|
|
2670
|
+
if (!candidates.some((r) => matchesRelation(r, match, graph))) {
|
|
2671
|
+
return false;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return true;
|
|
2675
|
+
}
|
|
2676
|
+
function matchesRelation(r, match, graph) {
|
|
2677
|
+
if (match.target !== void 0 && r.target !== match.target) return false;
|
|
2678
|
+
if (match.target_type !== void 0) {
|
|
2679
|
+
const tgt = graph.nodes.get(r.target);
|
|
2680
|
+
if (!tgt || tgt.meta.type !== match.target_type) return false;
|
|
2681
|
+
}
|
|
2682
|
+
if (match.consumes_port !== void 0) {
|
|
2683
|
+
if (!r.consumes || !r.consumes.includes(match.consumes_port)) return false;
|
|
2684
|
+
}
|
|
2685
|
+
return true;
|
|
2686
|
+
}
|
|
2687
|
+
function evaluateDescendantsClause(dc, node, graph) {
|
|
2688
|
+
const descendants = collectDescendants(node);
|
|
2689
|
+
if (descendants.length === 0) return false;
|
|
2690
|
+
if (dc.type !== void 0) {
|
|
2691
|
+
if (!descendants.some((d) => d.meta.type === dc.type)) return false;
|
|
2692
|
+
}
|
|
2693
|
+
if (dc.has_port !== void 0) {
|
|
2694
|
+
if (!descendants.some((d) => d.meta.ports && Object.prototype.hasOwnProperty.call(d.meta.ports, dc.has_port))) {
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
if (dc.relations) {
|
|
2699
|
+
if (!descendants.some((d) => evaluateRelationClause(dc.relations, d.meta.relations ?? [], graph))) {
|
|
2700
|
+
return false;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return true;
|
|
2704
|
+
}
|
|
2705
|
+
function evaluateNodeClause(nc, node) {
|
|
2706
|
+
if (nc.type !== void 0 && node.meta.type !== nc.type) return false;
|
|
2707
|
+
if (nc.has_port !== void 0) {
|
|
2708
|
+
if (!node.meta.ports || !Object.prototype.hasOwnProperty.call(node.meta.ports, nc.has_port)) return false;
|
|
2709
|
+
}
|
|
2710
|
+
if (nc.has_mapping !== void 0) {
|
|
2711
|
+
const has = (node.meta.mapping?.length ?? 0) > 0;
|
|
2712
|
+
if (has !== nc.has_mapping) return false;
|
|
2713
|
+
}
|
|
2714
|
+
return true;
|
|
2715
|
+
}
|
|
2716
|
+
function collectDescendants(node) {
|
|
2717
|
+
const out = [];
|
|
2718
|
+
const queue = [...node.children];
|
|
2719
|
+
while (queue.length > 0) {
|
|
2720
|
+
const curr = queue.shift();
|
|
2721
|
+
out.push(curr);
|
|
2722
|
+
for (const c of curr.children) queue.push(c);
|
|
2723
|
+
}
|
|
2724
|
+
return out;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2532
2727
|
// src/core/effective-aspects.ts
|
|
2533
2728
|
function computeEffectiveAspects(node, graph) {
|
|
2534
|
-
const
|
|
2729
|
+
const direct = /* @__PURE__ */ new Set();
|
|
2730
|
+
const ancestors = collectAncestors(node);
|
|
2731
|
+
const tryAdd = (aspectId, attachWhen) => {
|
|
2732
|
+
const aspectDef = graph.aspects.find((a) => a.id === aspectId);
|
|
2733
|
+
const globalWhen = aspectDef?.when;
|
|
2734
|
+
if (globalWhen && !evaluateWhen(globalWhen, node, graph)) {
|
|
2735
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${aspectId}' filtered: global when=false`);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (attachWhen && !evaluateWhen(attachWhen, node, graph)) {
|
|
2739
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${aspectId}' filtered: attach-site when=false`);
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
direct.add(aspectId);
|
|
2743
|
+
};
|
|
2535
2744
|
for (const id of node.meta.aspects ?? []) {
|
|
2536
|
-
|
|
2745
|
+
tryAdd(id, node.meta.aspectWhens?.[id]);
|
|
2537
2746
|
}
|
|
2538
|
-
const ancestors = collectAncestors(node);
|
|
2539
2747
|
for (const ancestor of ancestors) {
|
|
2540
2748
|
for (const id of ancestor.meta.aspects ?? []) {
|
|
2541
|
-
|
|
2749
|
+
tryAdd(id, ancestor.meta.aspectWhens?.[id]);
|
|
2542
2750
|
}
|
|
2543
2751
|
}
|
|
2544
2752
|
if (graph.architecture) {
|
|
2545
2753
|
const typeDef = graph.architecture.node_types[node.meta.type];
|
|
2546
2754
|
for (const id of typeDef?.aspects ?? []) {
|
|
2547
|
-
|
|
2755
|
+
tryAdd(id, typeDef?.aspectWhens?.[id]);
|
|
2548
2756
|
}
|
|
2549
2757
|
}
|
|
2550
2758
|
if (graph.architecture) {
|
|
2551
2759
|
for (const ancestor of ancestors) {
|
|
2552
2760
|
const typeDef = graph.architecture.node_types[ancestor.meta.type];
|
|
2553
2761
|
for (const id of typeDef?.aspects ?? []) {
|
|
2554
|
-
|
|
2762
|
+
tryAdd(id, typeDef?.aspectWhens?.[id]);
|
|
2555
2763
|
}
|
|
2556
2764
|
}
|
|
2557
2765
|
}
|
|
2558
2766
|
const allPaths = /* @__PURE__ */ new Set([node.path, ...ancestors.map((a) => a.path)]);
|
|
2559
2767
|
for (const flow of graph.flows) {
|
|
2560
|
-
if (flow.nodes.some((n) => allPaths.has(n)))
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
}
|
|
2768
|
+
if (!flow.nodes.some((n) => allPaths.has(n))) continue;
|
|
2769
|
+
for (const id of flow.aspects ?? []) {
|
|
2770
|
+
tryAdd(id, flow.aspectWhens?.[id]);
|
|
2564
2771
|
}
|
|
2565
2772
|
}
|
|
2566
2773
|
if (node.meta.relations) {
|
|
2567
2774
|
for (const relation of node.meta.relations) {
|
|
2568
2775
|
const targetNode = graph.nodes.get(relation.target);
|
|
2569
2776
|
if (!targetNode) continue;
|
|
2570
|
-
if (relation.consumes
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2777
|
+
if (!relation.consumes || !targetNode.meta.ports) continue;
|
|
2778
|
+
for (const portName of relation.consumes) {
|
|
2779
|
+
const port = targetNode.meta.ports[portName];
|
|
2780
|
+
if (!port?.aspects) continue;
|
|
2781
|
+
for (const id of port.aspects) {
|
|
2782
|
+
tryAdd(id, port.aspectWhens?.[id]);
|
|
2578
2783
|
}
|
|
2579
2784
|
}
|
|
2580
2785
|
}
|
|
2581
2786
|
}
|
|
2582
|
-
return
|
|
2787
|
+
return expandImpliesFiltered(direct, node, graph);
|
|
2788
|
+
}
|
|
2789
|
+
function expandImpliesFiltered(directIds, node, graph) {
|
|
2790
|
+
const idToAspect = /* @__PURE__ */ new Map();
|
|
2791
|
+
for (const a of graph.aspects) idToAspect.set(a.id, a);
|
|
2792
|
+
const result = /* @__PURE__ */ new Set();
|
|
2793
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2794
|
+
const stack = /* @__PURE__ */ new Set();
|
|
2795
|
+
const visit = (id, implierId) => {
|
|
2796
|
+
if (stack.has(id)) {
|
|
2797
|
+
throw new Error(`Aspect implies cycle detected involving aspect '${id}'`);
|
|
2798
|
+
}
|
|
2799
|
+
if (visited.has(id)) return;
|
|
2800
|
+
const aspectDef = idToAspect.get(id);
|
|
2801
|
+
if (aspectDef?.when && !evaluateWhen(aspectDef.when, node, graph)) {
|
|
2802
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${id}' filtered: global when=false (implies path)`);
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
if (implierId) {
|
|
2806
|
+
const implierDef = idToAspect.get(implierId);
|
|
2807
|
+
const perImplies = implierDef?.impliesWhens?.[id];
|
|
2808
|
+
if (perImplies && !evaluateWhen(perImplies, node, graph)) {
|
|
2809
|
+
debugWrite(`[effective-aspects] node '${node.path}' aspect '${id}' filtered: impliesWhens from '${implierId}' is false`);
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
stack.add(id);
|
|
2814
|
+
visited.add(id);
|
|
2815
|
+
result.add(id);
|
|
2816
|
+
const implies = aspectDef?.implies;
|
|
2817
|
+
if (implies) {
|
|
2818
|
+
for (const implied of implies) {
|
|
2819
|
+
visit(implied, id);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
stack.delete(id);
|
|
2823
|
+
};
|
|
2824
|
+
for (const id of directIds) visit(id, null);
|
|
2825
|
+
return result;
|
|
2583
2826
|
}
|
|
2584
2827
|
function getAspectSource(aspectId, node, graph) {
|
|
2585
2828
|
if (node.meta.aspects?.includes(aspectId)) {
|
|
@@ -2646,37 +2889,6 @@ function collectAncestors(node) {
|
|
|
2646
2889
|
}
|
|
2647
2890
|
return ancestors;
|
|
2648
2891
|
}
|
|
2649
|
-
function expandImplies(aspectIds, graph) {
|
|
2650
|
-
const idToImplies = /* @__PURE__ */ new Map();
|
|
2651
|
-
for (const aspect of graph.aspects) {
|
|
2652
|
-
if (aspect.implies) {
|
|
2653
|
-
idToImplies.set(aspect.id, aspect.implies);
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
const result = /* @__PURE__ */ new Set();
|
|
2657
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2658
|
-
const stack = /* @__PURE__ */ new Set();
|
|
2659
|
-
function collect(id) {
|
|
2660
|
-
if (stack.has(id)) {
|
|
2661
|
-
throw new Error(`Aspect implies cycle detected involving aspect '${id}'`);
|
|
2662
|
-
}
|
|
2663
|
-
if (visited.has(id)) return;
|
|
2664
|
-
stack.add(id);
|
|
2665
|
-
visited.add(id);
|
|
2666
|
-
result.add(id);
|
|
2667
|
-
const implies = idToImplies.get(id);
|
|
2668
|
-
if (implies) {
|
|
2669
|
-
for (const implied of implies) {
|
|
2670
|
-
collect(implied);
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
stack.delete(id);
|
|
2674
|
-
}
|
|
2675
|
-
for (const id of aspectIds) {
|
|
2676
|
-
collect(id);
|
|
2677
|
-
}
|
|
2678
|
-
return result;
|
|
2679
|
-
}
|
|
2680
2892
|
|
|
2681
2893
|
// src/core/context-builder.ts
|
|
2682
2894
|
var STRUCTURAL_RELATION_TYPES = /* @__PURE__ */ new Set(["uses", "calls", "extends", "implements"]);
|
|
@@ -3069,13 +3281,6 @@ async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
|
3069
3281
|
return result;
|
|
3070
3282
|
}
|
|
3071
3283
|
|
|
3072
|
-
// src/formatters/message-builder.ts
|
|
3073
|
-
function buildIssueMessage(msg) {
|
|
3074
|
-
return `${msg.what}
|
|
3075
|
-
${msg.why}
|
|
3076
|
-
${msg.next}`;
|
|
3077
|
-
}
|
|
3078
|
-
|
|
3079
3284
|
// src/core/validator.ts
|
|
3080
3285
|
async function validate(graph, scope = "all") {
|
|
3081
3286
|
const issues = [];
|
|
@@ -3126,6 +3331,7 @@ async function validate(graph, scope = "all") {
|
|
|
3126
3331
|
issues.push(...checkPortAspectsDefined(graph));
|
|
3127
3332
|
issues.push(...checkPortConsumes(graph));
|
|
3128
3333
|
issues.push(...checkOrphanedAspects(graph));
|
|
3334
|
+
issues.push(...checkWhenReferences(graph));
|
|
3129
3335
|
let filtered = issues;
|
|
3130
3336
|
let nodesScanned = graph.nodes.size;
|
|
3131
3337
|
const normalizedScope = scope.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -3943,6 +4149,141 @@ function checkOrphanedAspects(graph) {
|
|
|
3943
4149
|
}
|
|
3944
4150
|
return issues;
|
|
3945
4151
|
}
|
|
4152
|
+
function checkWhenReferences(graph) {
|
|
4153
|
+
const issues = [];
|
|
4154
|
+
const knownTypes = new Set(Object.keys(graph.architecture?.node_types ?? {}));
|
|
4155
|
+
const visitPredicate = (p2, ctx) => {
|
|
4156
|
+
if ("all_of" in p2) {
|
|
4157
|
+
p2.all_of.forEach((c, i) => visitPredicate(c, `${ctx}/all_of[${i}]`));
|
|
4158
|
+
return;
|
|
4159
|
+
}
|
|
4160
|
+
if ("any_of" in p2) {
|
|
4161
|
+
p2.any_of.forEach((c, i) => visitPredicate(c, `${ctx}/any_of[${i}]`));
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
if ("not" in p2) {
|
|
4165
|
+
visitPredicate(p2.not, `${ctx}/not`);
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
visitAtomic(p2, ctx);
|
|
4169
|
+
};
|
|
4170
|
+
const visitAtomic = (a, ctx) => {
|
|
4171
|
+
if (a.relations) visitRelationClause(a.relations, `${ctx}/relations`);
|
|
4172
|
+
if (a.descendants) visitDescendantsClause(a.descendants, `${ctx}/descendants`);
|
|
4173
|
+
if (a.node) visitNodeClause(a.node, `${ctx}/node`);
|
|
4174
|
+
};
|
|
4175
|
+
const visitRelationClause = (rc, ctx) => {
|
|
4176
|
+
for (const [relType, match] of Object.entries(rc)) {
|
|
4177
|
+
if (!match) continue;
|
|
4178
|
+
if (match.target_type !== void 0 && !knownTypes.has(match.target_type)) {
|
|
4179
|
+
issues.push({
|
|
4180
|
+
severity: "error",
|
|
4181
|
+
code: "when-unknown-type",
|
|
4182
|
+
rule: "when-unknown-type",
|
|
4183
|
+
message: buildIssueMessage({
|
|
4184
|
+
what: `Unknown node type '${match.target_type}' in when at ${ctx}/${relType}.target_type.`,
|
|
4185
|
+
why: "The predicate references a type that is not defined in yg-architecture.yaml; it will never evaluate.",
|
|
4186
|
+
next: `Fix the type name or define it in yg-architecture.yaml. Known types: ${Array.from(knownTypes).join(", ")}.`
|
|
4187
|
+
})
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
if (match.target !== void 0 && !graph.nodes.has(match.target)) {
|
|
4191
|
+
issues.push({
|
|
4192
|
+
severity: "error",
|
|
4193
|
+
code: "when-unknown-node",
|
|
4194
|
+
rule: "when-unknown-node",
|
|
4195
|
+
message: buildIssueMessage({
|
|
4196
|
+
what: `Referenced node '${match.target}' in when at ${ctx}/${relType}.target does not exist.`,
|
|
4197
|
+
why: "The predicate targets a node that is not in the graph.",
|
|
4198
|
+
next: `Fix the node path or add the node under .yggdrasil/model/.`
|
|
4199
|
+
})
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
if (match.consumes_port !== void 0 && match.target !== void 0) {
|
|
4203
|
+
const tgt = graph.nodes.get(match.target);
|
|
4204
|
+
if (tgt && !(tgt.meta.ports && match.consumes_port in tgt.meta.ports)) {
|
|
4205
|
+
issues.push({
|
|
4206
|
+
severity: "error",
|
|
4207
|
+
code: "when-unknown-port",
|
|
4208
|
+
rule: "when-unknown-port",
|
|
4209
|
+
message: buildIssueMessage({
|
|
4210
|
+
what: `Port '${match.consumes_port}' is not declared on node '${match.target}' in when at ${ctx}/${relType}.consumes_port.`,
|
|
4211
|
+
why: "The predicate references a port that does not exist on the target node.",
|
|
4212
|
+
next: `Fix the port name or add it to .yggdrasil/model/${match.target}/yg-node.yaml.`
|
|
4213
|
+
})
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
};
|
|
4219
|
+
const visitDescendantsClause = (dc, ctx) => {
|
|
4220
|
+
if (dc.relations) visitRelationClause(dc.relations, `${ctx}/relations`);
|
|
4221
|
+
if (dc.type !== void 0 && !knownTypes.has(dc.type)) {
|
|
4222
|
+
issues.push({
|
|
4223
|
+
severity: "error",
|
|
4224
|
+
code: "when-unknown-type",
|
|
4225
|
+
rule: "when-unknown-type",
|
|
4226
|
+
message: buildIssueMessage({
|
|
4227
|
+
what: `Unknown node type '${dc.type}' in when at ${ctx}/type.`,
|
|
4228
|
+
why: "The predicate references a type that is not defined in yg-architecture.yaml.",
|
|
4229
|
+
next: `Fix the type name or define it in yg-architecture.yaml. Known types: ${Array.from(knownTypes).join(", ")}.`
|
|
4230
|
+
})
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
};
|
|
4234
|
+
const visitNodeClause = (nc, ctx) => {
|
|
4235
|
+
if (nc.type !== void 0 && !knownTypes.has(nc.type)) {
|
|
4236
|
+
issues.push({
|
|
4237
|
+
severity: "error",
|
|
4238
|
+
code: "when-unknown-type",
|
|
4239
|
+
rule: "when-unknown-type",
|
|
4240
|
+
message: buildIssueMessage({
|
|
4241
|
+
what: `Unknown node type '${nc.type}' in when at ${ctx}/type.`,
|
|
4242
|
+
why: "The predicate references a type that is not defined in yg-architecture.yaml.",
|
|
4243
|
+
next: `Fix the type name or define it in yg-architecture.yaml. Known types: ${Array.from(knownTypes).join(", ")}.`
|
|
4244
|
+
})
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
};
|
|
4248
|
+
for (const aspect of graph.aspects) {
|
|
4249
|
+
if (aspect.when) visitPredicate(aspect.when, `aspect '${aspect.id}' when`);
|
|
4250
|
+
if (aspect.impliesWhens) {
|
|
4251
|
+
for (const [targetId, pred] of Object.entries(aspect.impliesWhens)) {
|
|
4252
|
+
visitPredicate(pred, `aspect '${aspect.id}' implies[${targetId}] when`);
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
if (graph.architecture) {
|
|
4257
|
+
for (const [typeName, typeDef] of Object.entries(graph.architecture.node_types)) {
|
|
4258
|
+
if (!typeDef.aspectWhens) continue;
|
|
4259
|
+
for (const [aspectId, pred] of Object.entries(typeDef.aspectWhens)) {
|
|
4260
|
+
visitPredicate(pred, `architecture node_types.${typeName} aspectWhens[${aspectId}]`);
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
for (const [nodePath, node] of graph.nodes) {
|
|
4265
|
+
if (node.meta.aspectWhens) {
|
|
4266
|
+
for (const [aspectId, pred] of Object.entries(node.meta.aspectWhens)) {
|
|
4267
|
+
visitPredicate(pred, `node '${nodePath}' aspectWhens[${aspectId}]`);
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
if (node.meta.ports) {
|
|
4271
|
+
for (const [portName, portDef] of Object.entries(node.meta.ports)) {
|
|
4272
|
+
if (!portDef.aspectWhens) continue;
|
|
4273
|
+
for (const [aspectId, pred] of Object.entries(portDef.aspectWhens)) {
|
|
4274
|
+
visitPredicate(pred, `node '${nodePath}' ports.${portName} aspectWhens[${aspectId}]`);
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
for (const flow of graph.flows) {
|
|
4280
|
+
if (!flow.aspectWhens) continue;
|
|
4281
|
+
for (const [aspectId, pred] of Object.entries(flow.aspectWhens)) {
|
|
4282
|
+
visitPredicate(pred, `flow '${flow.path}' aspectWhens[${aspectId}]`);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
return issues;
|
|
4286
|
+
}
|
|
3946
4287
|
|
|
3947
4288
|
// src/cli/owner.ts
|
|
3948
4289
|
import path15 from "path";
|
|
@@ -4240,12 +4581,14 @@ async function writeNodeDriftState(yggRoot, nodePath, nodeState) {
|
|
|
4240
4581
|
const content = JSON.stringify(nodeState, null, 2) + "\n";
|
|
4241
4582
|
await writeFile5(filePath, content, "utf-8");
|
|
4242
4583
|
}
|
|
4243
|
-
async function garbageCollectDriftState(yggRoot, validNodePaths) {
|
|
4584
|
+
async function garbageCollectDriftState(yggRoot, validNodePaths, shouldKeep) {
|
|
4244
4585
|
const driftDir = path16.join(yggRoot, DRIFT_STATE_DIR);
|
|
4245
4586
|
const allNodePaths = await scanJsonFiles(driftDir, driftDir);
|
|
4246
4587
|
const removed = [];
|
|
4247
4588
|
for (const nodePath of allNodePaths) {
|
|
4248
|
-
|
|
4589
|
+
const inGraph = validNodePaths.has(nodePath);
|
|
4590
|
+
const keep = inGraph && (shouldKeep ? shouldKeep(nodePath) : true);
|
|
4591
|
+
if (!keep) {
|
|
4249
4592
|
const filePath = nodeStatePath(yggRoot, nodePath);
|
|
4250
4593
|
await rm2(filePath);
|
|
4251
4594
|
await removeEmptyParents(filePath, driftDir);
|
|
@@ -4524,7 +4867,16 @@ function getChildMappingExclusions(graph, nodePath) {
|
|
|
4524
4867
|
}
|
|
4525
4868
|
async function runGC(graph) {
|
|
4526
4869
|
const validPaths = new Set(graph.nodes.keys());
|
|
4527
|
-
return garbageCollectDriftState(
|
|
4870
|
+
return garbageCollectDriftState(
|
|
4871
|
+
graph.rootPath,
|
|
4872
|
+
validPaths,
|
|
4873
|
+
(nodePath) => {
|
|
4874
|
+
const node = graph.nodes.get(nodePath);
|
|
4875
|
+
if (!node) return false;
|
|
4876
|
+
const effective = computeEffectiveAspects(node, graph);
|
|
4877
|
+
return effective.size > 0;
|
|
4878
|
+
}
|
|
4879
|
+
);
|
|
4528
4880
|
}
|
|
4529
4881
|
async function commitApproval(yggRoot, result) {
|
|
4530
4882
|
if (result.pendingDriftState) {
|
|
@@ -4824,6 +5176,15 @@ async function runCheck(graph, gitTrackedFiles) {
|
|
|
4824
5176
|
coverageIssue = buildCoverageIssue(uncovered, totalFiles);
|
|
4825
5177
|
}
|
|
4826
5178
|
const orphanedPaths = await detectOrphanedDriftState(graph);
|
|
5179
|
+
await garbageCollectDriftState(
|
|
5180
|
+
graph.rootPath,
|
|
5181
|
+
new Set(graph.nodes.keys()),
|
|
5182
|
+
(nodePath) => {
|
|
5183
|
+
const node = graph.nodes.get(nodePath);
|
|
5184
|
+
if (!node) return false;
|
|
5185
|
+
return computeEffectiveAspects(node, graph).size > 0;
|
|
5186
|
+
}
|
|
5187
|
+
);
|
|
4827
5188
|
const yggRelative = path19.relative(path19.dirname(graph.rootPath), graph.rootPath).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4828
5189
|
const orphanWarnings = orphanedPaths.map((p2) => ({
|
|
4829
5190
|
severity: "warning",
|
|
@@ -5472,26 +5833,57 @@ async function loadSecrets(rootPath, providerName) {
|
|
|
5472
5833
|
return void 0;
|
|
5473
5834
|
}
|
|
5474
5835
|
const raw = parseYaml9(content);
|
|
5475
|
-
if (
|
|
5476
|
-
if (
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
}
|
|
5484
|
-
return void 0;
|
|
5485
|
-
|
|
5486
|
-
|
|
5836
|
+
if (raw === null || raw === void 0) return void 0;
|
|
5837
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
5838
|
+
throw new Error(`yg-secrets.yaml: top level must be a YAML mapping`);
|
|
5839
|
+
}
|
|
5840
|
+
const rawObj = raw;
|
|
5841
|
+
if (rawObj.reviewer === void 0) return void 0;
|
|
5842
|
+
if (typeof rawObj.reviewer !== "object" || rawObj.reviewer === null || Array.isArray(rawObj.reviewer)) {
|
|
5843
|
+
throw new Error(`yg-secrets.yaml: 'reviewer' must be a YAML mapping`);
|
|
5844
|
+
}
|
|
5845
|
+
if (!providerName) return void 0;
|
|
5846
|
+
const reviewerRaw = rawObj.reviewer;
|
|
5847
|
+
const providerSection = reviewerRaw[providerName];
|
|
5848
|
+
if (providerSection === void 0) return void 0;
|
|
5849
|
+
if (typeof providerSection !== "object" || providerSection === null || Array.isArray(providerSection)) {
|
|
5850
|
+
throw new Error(`yg-secrets.yaml: 'reviewer.${providerName}' must be a YAML mapping`);
|
|
5851
|
+
}
|
|
5852
|
+
return extractSecretFields(providerSection, providerName);
|
|
5853
|
+
}
|
|
5854
|
+
function extractSecretFields(raw, providerName) {
|
|
5855
|
+
const ctx = (field) => `yg-secrets.yaml at reviewer.${providerName}.${field}`;
|
|
5487
5856
|
const partial = {};
|
|
5488
|
-
if (
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
if (
|
|
5493
|
-
|
|
5494
|
-
|
|
5857
|
+
if (raw.api_key !== void 0) {
|
|
5858
|
+
if (typeof raw.api_key !== "string") throw new Error(`${ctx("api_key")}: must be a string`);
|
|
5859
|
+
partial.api_key = raw.api_key;
|
|
5860
|
+
}
|
|
5861
|
+
if (raw.provider !== void 0) {
|
|
5862
|
+
if (typeof raw.provider !== "string") throw new Error(`${ctx("provider")}: must be a string`);
|
|
5863
|
+
partial.provider = raw.provider;
|
|
5864
|
+
}
|
|
5865
|
+
if (raw.model !== void 0) {
|
|
5866
|
+
if (typeof raw.model !== "string") throw new Error(`${ctx("model")}: must be a string`);
|
|
5867
|
+
partial.model = raw.model;
|
|
5868
|
+
}
|
|
5869
|
+
if (raw.endpoint !== void 0) {
|
|
5870
|
+
if (typeof raw.endpoint !== "string") throw new Error(`${ctx("endpoint")}: must be a string`);
|
|
5871
|
+
partial.endpoint = raw.endpoint;
|
|
5872
|
+
}
|
|
5873
|
+
if (raw.temperature !== void 0) {
|
|
5874
|
+
if (typeof raw.temperature !== "number") throw new Error(`${ctx("temperature")}: must be a number`);
|
|
5875
|
+
partial.temperature = raw.temperature;
|
|
5876
|
+
}
|
|
5877
|
+
if (raw.consensus !== void 0) {
|
|
5878
|
+
if (typeof raw.consensus !== "number") throw new Error(`${ctx("consensus")}: must be a number`);
|
|
5879
|
+
partial.consensus = raw.consensus;
|
|
5880
|
+
}
|
|
5881
|
+
if (raw.max_tokens !== void 0) {
|
|
5882
|
+
if (typeof raw.max_tokens !== "number" && raw.max_tokens !== "auto") {
|
|
5883
|
+
throw new Error(`${ctx("max_tokens")}: must be a number or 'auto'`);
|
|
5884
|
+
}
|
|
5885
|
+
partial.max_tokens = raw.max_tokens;
|
|
5886
|
+
}
|
|
5495
5887
|
return Object.keys(partial).length > 0 ? partial : void 0;
|
|
5496
5888
|
}
|
|
5497
5889
|
function mergeLlmConfig(base, secrets) {
|
|
@@ -5988,7 +6380,7 @@ function buildTransitiveChains(targetNode, direct, allDependents, reverse) {
|
|
|
5988
6380
|
}
|
|
5989
6381
|
return chains.sort();
|
|
5990
6382
|
}
|
|
5991
|
-
function
|
|
6383
|
+
function collectDescendants2(graph, nodePath) {
|
|
5992
6384
|
const node = graph.nodes.get(nodePath);
|
|
5993
6385
|
if (!node) return [];
|
|
5994
6386
|
const result = [];
|
|
@@ -6142,7 +6534,7 @@ async function handleFlowImpact(graph, flowName) {
|
|
|
6142
6534
|
for (const nodePath of flow.nodes) {
|
|
6143
6535
|
if (graph.nodes.has(nodePath)) {
|
|
6144
6536
|
participants.add(nodePath);
|
|
6145
|
-
for (const desc of
|
|
6537
|
+
for (const desc of collectDescendants2(graph, nodePath)) {
|
|
6146
6538
|
participants.add(desc);
|
|
6147
6539
|
}
|
|
6148
6540
|
}
|
|
@@ -6318,7 +6710,7 @@ function registerImpactCommand(program2) {
|
|
|
6318
6710
|
`);
|
|
6319
6711
|
}
|
|
6320
6712
|
}
|
|
6321
|
-
const descendants =
|
|
6713
|
+
const descendants = collectDescendants2(graph, nodePath);
|
|
6322
6714
|
if (descendants.length > 0) {
|
|
6323
6715
|
process.stdout.write("\nDescendants (hierarchy impact):\n");
|
|
6324
6716
|
for (const desc of descendants) {
|