@chrisdudek/yg 4.0.1 → 4.1.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 +331 -575
- package/graph-schemas/yg-architecture.yaml +33 -6
- package/graph-schemas/yg-aspect.yaml +16 -5
- package/graph-schemas/yg-flow.yaml +16 -4
- package/graph-schemas/yg-node.yaml +20 -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>
|
|
@@ -153,437 +160,200 @@ import { readFile, writeFile, mkdir } from "fs/promises";
|
|
|
153
160
|
import path from "path";
|
|
154
161
|
|
|
155
162
|
// 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.
|
|
163
|
+
var SYSTEM = `## SYSTEM
|
|
166
164
|
|
|
167
|
-
|
|
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.
|
|
165
|
+
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.
|
|
173
166
|
|
|
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>
|
|
167
|
+
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.
|
|
177
168
|
|
|
178
|
-
|
|
179
|
-
</EXTREMELY-IMPORTANT>
|
|
180
|
-
|
|
181
|
-
Yggdrasil is continuous architecture enforcement stored in \`.yggdrasil/\`. It maps the repository and verifies source code against architectural rules (aspects) at approve time.
|
|
182
|
-
|
|
183
|
-
### Quick Start
|
|
169
|
+
### Graph Elements
|
|
184
170
|
|
|
185
171
|
\`\`\`
|
|
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.
|
|
172
|
+
.yggdrasil/
|
|
173
|
+
yg-architecture.yaml \u2190 node type definitions, default aspects per type, allowed relations
|
|
174
|
+
yg-config.yaml \u2190 reviewer config, quality thresholds, parallelism
|
|
175
|
+
model/ \u2190 nodes: what exists \u2014 hierarchy, relations, file mappings
|
|
176
|
+
aspects/ \u2190 aspects: what must be satisfied \u2014 enforceable rules
|
|
177
|
+
flows/ \u2190 flows: business processes with node participation
|
|
178
|
+
schemas/ \u2190 YAML schemas \u2014 read before creating any graph element
|
|
179
|
+
.drift-state/ \u2190 generated by CLI; never edit manually
|
|
210
180
|
\`\`\`
|
|
211
181
|
|
|
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.
|
|
182
|
+
**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.
|
|
232
183
|
|
|
233
|
-
|
|
184
|
+
**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).
|
|
234
185
|
|
|
235
|
-
-
|
|
236
|
-
- Option B \u2014 Abort
|
|
186
|
+
**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.
|
|
237
187
|
|
|
238
|
-
|
|
188
|
+
**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.
|
|
239
189
|
|
|
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\`
|
|
190
|
+
**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.
|
|
246
191
|
|
|
247
|
-
**
|
|
192
|
+
**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\`.
|
|
248
193
|
|
|
249
|
-
|
|
194
|
+
### How Aspects Reach a Node \u2014 7 Channels
|
|
250
195
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
After the user chooses, return to Step 1 and follow Step 2a.
|
|
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
|
|
196
|
+
Aspects accumulate from multiple sources simultaneously. The reviewer checks ALL of them \u2014 the node must satisfy every aspect regardless of origin.
|
|
265
197
|
|
|
266
198
|
\`\`\`
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
-
|
|
283
|
-
-
|
|
199
|
+
EXAMPLE: node "orders/handler" (type: command, child of "orders")
|
|
200
|
+
|
|
201
|
+
Channel 1: OWN \u2014 node.aspects: [input-validation]
|
|
202
|
+
Channel 2: ANCESTOR \u2014 parent "orders" has aspects: [audit-logging]
|
|
203
|
+
Channel 3: OWN TYPE \u2014 architecture says type "command" \u2192 [cli-command-contract]
|
|
204
|
+
Channel 4: ANCESTOR TYPE \u2014 parent "orders" type "module" \u2192 [] (no defaults here)
|
|
205
|
+
Channel 5: FLOWS \u2014 flow "order-processing" includes "orders" \u2192 flow aspects: [deterministic]
|
|
206
|
+
Channel 6: PORTS \u2014 relation to "payments/service" consumes port "charge" \u2192 [correlation-tracking]
|
|
207
|
+
Channel 7: IMPLIED \u2014 aspect "audit-logging" implies: [diagnostic-logging]
|
|
208
|
+
|
|
209
|
+
EFFECTIVE ASPECTS for "orders/handler":
|
|
210
|
+
input-validation \u2190 own
|
|
211
|
+
audit-logging \u2190 parent "orders"
|
|
212
|
+
cli-command-contract \u2190 architecture type "command"
|
|
213
|
+
deterministic \u2190 flow "order-processing" (via parent "orders")
|
|
214
|
+
correlation-tracking \u2190 port "charge" on "payments/service"
|
|
215
|
+
diagnostic-logging \u2190 implied by "audit-logging"
|
|
284
216
|
\`\`\`
|
|
285
217
|
|
|
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
|
|
296
|
-
|
|
297
|
-
\`yg-architecture.yaml\` defines which node types exist, what each type means, and which relations are allowed between types. **Every change to this file requires user confirmation** \u2014 it defines the vocabulary and constraints for the entire graph.
|
|
298
|
-
|
|
299
|
-
**On a new or empty repo:** Do NOT accept the defaults silently. Read \`yg-architecture.yaml\`, present the current types and relation rules to the user, and ask: "Does this type system fit your project, or should we adjust it?" The default types are starting points, not answers.
|
|
300
|
-
|
|
301
|
-
**Before creating nodes with relations:** Read \`yg-architecture.yaml\` to check which relation types are allowed between the source and target node types. Do not guess \u2014 if the architecture does not allow \`service\` to call \`data\`, you cannot create that relation. The options are:
|
|
302
|
-
1. Use an allowed relation type
|
|
303
|
-
2. Ask the user whether to update the architecture (explain the constraint)
|
|
304
|
-
3. Change the node type to one that allows the needed relation
|
|
305
|
-
|
|
306
|
-
**Never silently modify \`yg-architecture.yaml\`.** If a relation is forbidden, do NOT add the target type to the allowed list without asking the user first. Present the constraint, explain the options, let the user decide.
|
|
307
|
-
|
|
308
|
-
### Delegating to Subagents
|
|
309
|
-
|
|
310
|
-
When you delegate work to a subagent (any subprocess, tool agent, or spawned assistant), the subagent does NOT inherit your Yggdrasil knowledge. Before any other instruction, the subagent MUST:
|
|
218
|
+
Consequences of this cascade:
|
|
219
|
+
- Adding an aspect to a parent applies it to ALL children. Check impact first: \`yg impact --aspect <id>\`.
|
|
220
|
+
- Adding a node to a flow with aspects means that node must satisfy flow aspects.
|
|
221
|
+
- Architecture default aspects apply to every node of that type automatically.
|
|
222
|
+
- Implies chains expand recursively. Cycles are forbidden \u2014 CLI detects them.
|
|
311
223
|
|
|
312
|
-
|
|
313
|
-
2. Follow the Quick Start Protocol from that file before touching any mapped code
|
|
224
|
+
### Reviewer
|
|
314
225
|
|
|
315
|
-
|
|
226
|
+
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
227
|
|
|
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
|
-
\`\`\`
|
|
228
|
+
- **Approved** \u2192 baseline recorded, drift cleared.
|
|
229
|
+
- **Refused** \u2192 violation report with what and where. Fix the code, re-run approve.
|
|
324
230
|
|
|
325
|
-
|
|
326
|
-
var REFERENCE = `## REFERENCE
|
|
231
|
+
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
232
|
|
|
328
|
-
###
|
|
233
|
+
### Drift and Cascade
|
|
329
234
|
|
|
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
|
-
\`\`\`
|
|
235
|
+
Drift = source code or upstream context changed since the last approve. The reviewer must verify again. \`yg check\` detects two kinds:
|
|
340
236
|
|
|
341
|
-
|
|
237
|
+
- **Source drift** \u2014 mapped source files were modified. Fix: \`yg approve --node <path>\`.
|
|
238
|
+
- **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
239
|
|
|
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.
|
|
240
|
+
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
241
|
|
|
348
|
-
|
|
242
|
+
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
243
|
|
|
350
|
-
|
|
244
|
+
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
245
|
|
|
352
|
-
|
|
246
|
+
### CLI Commands
|
|
353
247
|
|
|
354
|
-
|
|
|
248
|
+
| Command | Purpose |
|
|
355
249
|
|---|---|
|
|
356
|
-
|
|
|
357
|
-
|
|
|
358
|
-
|
|
|
359
|
-
|
|
|
360
|
-
|
|
|
361
|
-
|
|
362
|
-
|
|
250
|
+
| \`yg check\` | Unified gate \u2014 drift, validation, coverage, completeness. Blocks CI. |
|
|
251
|
+
| \`yg context --file <path>\` | Show owning node, effective aspects (\`read:\` paths), dependencies |
|
|
252
|
+
| \`yg context --node <path>\` | Show node overview \u2014 aspects, flows, dependents, source files |
|
|
253
|
+
| \`yg approve --node <path> [<path2>...]\` | Run reviewer on one or more nodes |
|
|
254
|
+
| \`yg approve --aspect <id>\` | Batch approve all nodes affected by this aspect change |
|
|
255
|
+
| \`yg approve --flow <name>\` | Batch approve all nodes in this flow |
|
|
256
|
+
| \`yg approve --dry-run --node <path>\` | Preview reviewer prompt without LLM call |
|
|
257
|
+
| \`yg impact --node <path>\` | Blast radius \u2014 dependents, flows, cascade scope |
|
|
258
|
+
| \`yg impact --file <path>\` | Blast radius for a specific file |
|
|
259
|
+
| \`yg impact --aspect <id>\` | All nodes affected by this aspect |
|
|
260
|
+
| \`yg impact --flow <name>\` | All nodes in this flow |
|
|
261
|
+
| \`yg tree [--root <path>] [--depth <n>]\` | Browse graph structure \u2014 all nodes with type and description |
|
|
262
|
+
| \`yg aspects\` | List all aspects with usage counts and sources |
|
|
263
|
+
| \`yg flows\` | List all flows with participants and aspects |
|
|
264
|
+
| \`yg owner --file <path>\` | Find which node owns a source file |
|
|
265
|
+
| \`yg init\` | Bootstrap or refresh \`.yggdrasil/\` setup |
|
|
363
266
|
|
|
364
|
-
|
|
267
|
+
### Impact and Cost
|
|
365
268
|
|
|
366
|
-
|
|
367
|
-
- **\`yg context --file <path>\`** \u2014 per-file: aspects to satisfy, consumed dependencies
|
|
269
|
+
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.
|
|
368
270
|
|
|
369
|
-
|
|
271
|
+
When code doesn't match an aspect, three options:
|
|
370
272
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
273
|
+
| Option | When | Cost |
|
|
274
|
+
|---|---|---|
|
|
275
|
+
| **Change code** \u2014 conform to aspect | Aspect is correct, code violates it | Proportional to files needing fixes |
|
|
276
|
+
| **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 |
|
|
277
|
+
| **Suppress** \u2014 \`yg-suppress\` waiver | Known tech debt, refactor not now | Zero approve cost, consciously accepted risk |
|
|
379
278
|
|
|
380
|
-
|
|
381
|
-
|
|
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
|
|
279
|
+
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.`;
|
|
280
|
+
var DECISIONS = `## DECISIONS
|
|
385
281
|
|
|
386
|
-
|
|
282
|
+
### Workflow
|
|
387
283
|
|
|
388
|
-
|
|
284
|
+
**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.
|
|
389
285
|
|
|
390
|
-
|
|
286
|
+
**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>\`.
|
|
391
287
|
|
|
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.
|
|
288
|
+
**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.
|
|
397
289
|
|
|
398
|
-
|
|
290
|
+
**End of conversation:** \`yg check\` \u2014 resolve all drift. \`yg check\` failures block CI. If drift remains, the build breaks.
|
|
399
291
|
|
|
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 |
|
|
292
|
+
**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.
|
|
409
293
|
|
|
410
|
-
|
|
294
|
+
**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.
|
|
411
295
|
|
|
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\`
|
|
296
|
+
### When to Create Graph Elements
|
|
417
297
|
|
|
418
|
-
|
|
298
|
+
**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.
|
|
419
299
|
|
|
420
|
-
|
|
300
|
+
**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.
|
|
421
301
|
|
|
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\`
|
|
302
|
+
**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.
|
|
426
303
|
|
|
427
|
-
|
|
304
|
+
**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.
|
|
428
305
|
|
|
429
|
-
|
|
306
|
+
### Aspect Discovery
|
|
430
307
|
|
|
431
|
-
|
|
308
|
+
Aspects emerge from patterns \u2014 greenfield and brownfield:
|
|
432
309
|
|
|
433
|
-
|
|
310
|
+
- After working on 3+ files in the same area: are you applying the same pattern? If yes, create an aspect.
|
|
311
|
+
- Watch for "invisible" aspects: audit logging, webhook dispatch, auth guards, job dispatch \u2014 cross-cutting but easy to miss.
|
|
312
|
+
- Brownfield: same utility called in 3+ files = aspect waiting to be created.
|
|
434
313
|
|
|
435
|
-
|
|
436
|
-
ports:
|
|
437
|
-
charge:
|
|
438
|
-
description: "Charge payment"
|
|
439
|
-
aspects: [correlation-tracking]
|
|
440
|
-
\`\`\`
|
|
314
|
+
### Delegating to Subagents
|
|
441
315
|
|
|
442
|
-
|
|
316
|
+
Subagents don't inherit Yggdrasil knowledge. First instruction in every subagent prompt:
|
|
443
317
|
|
|
444
|
-
\`\`\`
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
318
|
+
\`\`\`
|
|
319
|
+
BEFORE doing anything else: read .yggdrasil/agent-rules.md and follow its protocol.
|
|
320
|
+
DELIVERABLES \u2014 all required, incomplete work will be rejected:
|
|
321
|
+
1. Working source code
|
|
322
|
+
2. Graph nodes for every new/modified source file
|
|
323
|
+
3. \`yg check\` passing
|
|
449
324
|
\`\`\`
|
|
450
325
|
|
|
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.
|
|
326
|
+
Code without graph updates = incomplete work.
|
|
559
327
|
|
|
560
|
-
###
|
|
328
|
+
### \`yg-suppress\` \u2014 Inline Aspect Waiver
|
|
561
329
|
|
|
562
|
-
|
|
330
|
+
Source code comments with \`yg-suppress(<aspect-path>) <reason>\` waive a specific aspect. The reviewer honors these unconditionally.
|
|
563
331
|
|
|
564
|
-
|
|
565
|
-
-
|
|
566
|
-
-
|
|
567
|
-
-
|
|
568
|
-
|
|
569
|
-
- [ ] 6. \`yg check\`, \`yg approve\` per node
|
|
570
|
-
- [ ] 7. Proceed with user's original request
|
|
332
|
+
\`\`\`
|
|
333
|
+
// yg-suppress(cqrs/single-responsibility) brownfield handler, refactor planned
|
|
334
|
+
# yg-suppress(security/input-validation) static config, no user input
|
|
335
|
+
<!-- yg-suppress(accessibility/aria-labels) generated markup, tracked in JIRA-456 -->
|
|
336
|
+
\`\`\`
|
|
571
337
|
|
|
572
|
-
|
|
338
|
+
- You may propose a suppress when you see brownfield code or known tech debt violating an aspect
|
|
339
|
+
- You MUST NEVER write a suppress without explicit user confirmation \u2014 no exceptions
|
|
340
|
+
- Provide the correct aspect-path from graph context, ask the user for the reason
|
|
341
|
+
- You do not invent reasons \u2014 the user provides or approves them
|
|
342
|
+
- The marker applies contextually to surrounding code (function, class, block). At file level, it applies to the entire file.
|
|
573
343
|
|
|
574
344
|
### Escape Hatch
|
|
575
345
|
|
|
576
|
-
If the user explicitly requests a code-only change
|
|
346
|
+
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
347
|
|
|
578
|
-
|
|
579
|
-
- Do NOT run \`yg approve\` \u2014 leave the drift visible.
|
|
348
|
+
### Operational Notes
|
|
580
349
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
350
|
+
- English only for all files in \`.yggdrasil/\`. Conversation can be any language.
|
|
351
|
+
- Read the relevant schema from \`schemas/\` before creating any YAML file.
|
|
352
|
+
- Every \`yg-node.yaml\`, \`yg-aspect.yaml\`, and \`yg-flow.yaml\` needs a \`description\`. Write it when creating, update it when purpose changes.
|
|
353
|
+
- 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.
|
|
354
|
+
- When unsure about anything: ask the user. Do not guess. Do not assume.
|
|
355
|
+
- Never invent rationale for aspects. If you don't know why a requirement exists, ask.`;
|
|
356
|
+
var AGENT_RULES_CONTENT = [SYSTEM, DECISIONS].join("\n\n---\n\n") + "\n";
|
|
587
357
|
|
|
588
358
|
// src/templates/platform.ts
|
|
589
359
|
var AGENT_RULES_IMPORT = "@.yggdrasil/agent-rules.md";
|
|
@@ -1364,10 +1134,17 @@ var MIGRATIONS = [
|
|
|
1364
1134
|
];
|
|
1365
1135
|
|
|
1366
1136
|
// src/cli/init.ts
|
|
1367
|
-
function
|
|
1137
|
+
function getPackageRoot() {
|
|
1368
1138
|
const currentDir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1369
|
-
|
|
1370
|
-
|
|
1139
|
+
return path5.join(currentDir, "..");
|
|
1140
|
+
}
|
|
1141
|
+
function getGraphSchemasDir() {
|
|
1142
|
+
return path5.join(getPackageRoot(), "graph-schemas");
|
|
1143
|
+
}
|
|
1144
|
+
async function getCliVersion() {
|
|
1145
|
+
const pkgPath = path5.join(getPackageRoot(), "package.json");
|
|
1146
|
+
const pkg2 = JSON.parse(await readFile4(pkgPath, "utf-8"));
|
|
1147
|
+
return pkg2.version;
|
|
1371
1148
|
}
|
|
1372
1149
|
async function refreshSchemas(yggRoot) {
|
|
1373
1150
|
const schemasDir = path5.join(yggRoot, "schemas");
|
|
@@ -1669,7 +1446,7 @@ async function existingInit(projectRoot) {
|
|
|
1669
1446
|
}
|
|
1670
1447
|
p.intro(chalk.bold("Yggdrasil Configuration"));
|
|
1671
1448
|
const currentVersion = await detectVersion(yggRoot);
|
|
1672
|
-
const cliVersion =
|
|
1449
|
+
const cliVersion = await getCliVersion();
|
|
1673
1450
|
if (currentVersion && currentVersion !== cliVersion) {
|
|
1674
1451
|
const migrate = await p.confirm({
|
|
1675
1452
|
message: `Graph version ${currentVersion} detected \u2014 CLI is ${cliVersion}. Run migration?`,
|
|
@@ -1767,6 +1544,7 @@ function registerInitCommand(program2) {
|
|
|
1767
1544
|
} catch {
|
|
1768
1545
|
await writeFile4(architecturePath, DEFAULT_ARCHITECTURE, "utf-8");
|
|
1769
1546
|
}
|
|
1547
|
+
await updateConfigVersion(yggRoot, await getCliVersion());
|
|
1770
1548
|
const rulesPath = await installRulesForPlatform(projectRoot, options.platform);
|
|
1771
1549
|
process.stdout.write(`Rules and schemas refreshed: ${path5.relative(projectRoot, rulesPath)}
|
|
1772
1550
|
`);
|
|
@@ -2346,6 +2124,10 @@ function normalizeProjectRelativePath(projectRoot, rawPath) {
|
|
|
2346
2124
|
function projectRootFromGraph(yggRootPath) {
|
|
2347
2125
|
return path10.dirname(yggRootPath);
|
|
2348
2126
|
}
|
|
2127
|
+
function resolveFileArg(cwd, repoRoot, rawPath) {
|
|
2128
|
+
const absolute = path10.resolve(cwd, rawPath.trim());
|
|
2129
|
+
return path10.relative(repoRoot, absolute).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2130
|
+
}
|
|
2349
2131
|
|
|
2350
2132
|
// src/core/graph-loader.ts
|
|
2351
2133
|
function toModelPath(absolutePath, modelDir) {
|
|
@@ -2910,8 +2692,160 @@ function formatFileContext(data) {
|
|
|
2910
2692
|
}
|
|
2911
2693
|
|
|
2912
2694
|
// src/core/validator.ts
|
|
2913
|
-
import { readdir as
|
|
2695
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
2696
|
+
import path14 from "path";
|
|
2697
|
+
|
|
2698
|
+
// src/utils/hash.ts
|
|
2699
|
+
import { readFile as readFile14, readdir as readdir5, stat as stat4 } from "fs/promises";
|
|
2914
2700
|
import path13 from "path";
|
|
2701
|
+
import { createHash } from "crypto";
|
|
2702
|
+
import { createRequire } from "module";
|
|
2703
|
+
var require2 = createRequire(import.meta.url);
|
|
2704
|
+
var ignoreFactory = require2("ignore");
|
|
2705
|
+
async function hashFile(filePath) {
|
|
2706
|
+
const content = await readFile14(filePath);
|
|
2707
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2708
|
+
}
|
|
2709
|
+
async function loadRootGitignoreStack(projectRoot) {
|
|
2710
|
+
if (!projectRoot) return [];
|
|
2711
|
+
try {
|
|
2712
|
+
const content = await readFile14(path13.join(projectRoot, ".gitignore"), "utf-8");
|
|
2713
|
+
const matcher = ignoreFactory();
|
|
2714
|
+
matcher.add(content);
|
|
2715
|
+
return [{ basePath: projectRoot, matcher }];
|
|
2716
|
+
} catch {
|
|
2717
|
+
return [];
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
function isIgnoredByStack(candidatePath, stack) {
|
|
2721
|
+
for (const { basePath, matcher } of stack) {
|
|
2722
|
+
const relativePath = path13.relative(basePath, candidatePath);
|
|
2723
|
+
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
2724
|
+
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
2725
|
+
}
|
|
2726
|
+
return false;
|
|
2727
|
+
}
|
|
2728
|
+
function hashString(content) {
|
|
2729
|
+
return createHash("sha256").update(content).digest("hex");
|
|
2730
|
+
}
|
|
2731
|
+
async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes) {
|
|
2732
|
+
const fileHashes = {};
|
|
2733
|
+
const fileMtimes = {};
|
|
2734
|
+
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
2735
|
+
const allFiles = [];
|
|
2736
|
+
for (const tf of trackedFiles) {
|
|
2737
|
+
if (tf.syntheticHash) {
|
|
2738
|
+
fileHashes[tf.path] = tf.syntheticHash;
|
|
2739
|
+
continue;
|
|
2740
|
+
}
|
|
2741
|
+
const absPath = path13.join(projectRoot, tf.path);
|
|
2742
|
+
try {
|
|
2743
|
+
const st = await stat4(absPath);
|
|
2744
|
+
if (st.isDirectory()) {
|
|
2745
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2746
|
+
projectRoot,
|
|
2747
|
+
gitignoreStack
|
|
2748
|
+
});
|
|
2749
|
+
for (const entry of dirEntries) {
|
|
2750
|
+
allFiles.push({
|
|
2751
|
+
relPath: path13.join(tf.path, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
2752
|
+
absPath: entry.absPath,
|
|
2753
|
+
mtimeMs: entry.mtimeMs
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
} else {
|
|
2757
|
+
allFiles.push({ relPath: tf.path, absPath, mtimeMs: st.mtimeMs });
|
|
2758
|
+
}
|
|
2759
|
+
} catch {
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) => entry.relPath === prefix || entry.relPath.startsWith(prefix + "/"))) : allFiles;
|
|
2764
|
+
const dirty = [];
|
|
2765
|
+
for (const entry of filtered) {
|
|
2766
|
+
const storedMtime = storedFileData?.mtimes[entry.relPath];
|
|
2767
|
+
const storedHash = storedFileData?.hashes[entry.relPath];
|
|
2768
|
+
if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
|
|
2769
|
+
fileHashes[entry.relPath] = storedHash;
|
|
2770
|
+
} else {
|
|
2771
|
+
dirty.push(entry);
|
|
2772
|
+
}
|
|
2773
|
+
fileMtimes[entry.relPath] = entry.mtimeMs;
|
|
2774
|
+
}
|
|
2775
|
+
const BATCH_SIZE = 256;
|
|
2776
|
+
for (let i = 0; i < dirty.length; i += BATCH_SIZE) {
|
|
2777
|
+
const batch = dirty.slice(i, i + BATCH_SIZE);
|
|
2778
|
+
const hashes = await Promise.all(batch.map((e) => hashFile(e.absPath)));
|
|
2779
|
+
for (let j = 0; j < batch.length; j++) {
|
|
2780
|
+
fileHashes[batch[j].relPath] = hashes[j];
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
const sorted = Object.entries(fileHashes).sort(([a], [b]) => a.localeCompare(b));
|
|
2784
|
+
const digest = sorted.map(([p2, h]) => `${p2}:${h}`).join("\n");
|
|
2785
|
+
const canonicalHash = hashString(digest);
|
|
2786
|
+
return { canonicalHash, fileHashes, fileMtimes };
|
|
2787
|
+
}
|
|
2788
|
+
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
2789
|
+
let stack = options.gitignoreStack ?? [];
|
|
2790
|
+
try {
|
|
2791
|
+
const localContent = await readFile14(path13.join(directoryPath, ".gitignore"), "utf-8");
|
|
2792
|
+
const localMatcher = ignoreFactory();
|
|
2793
|
+
localMatcher.add(localContent);
|
|
2794
|
+
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
2795
|
+
} catch {
|
|
2796
|
+
}
|
|
2797
|
+
const entries = await readdir5(directoryPath, { withFileTypes: true });
|
|
2798
|
+
const dirs = [];
|
|
2799
|
+
const files = [];
|
|
2800
|
+
for (const entry of entries) {
|
|
2801
|
+
const absoluteChildPath = path13.join(directoryPath, entry.name);
|
|
2802
|
+
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
2803
|
+
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
2804
|
+
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
2805
|
+
}
|
|
2806
|
+
const [dirResults, fileStats] = await Promise.all([
|
|
2807
|
+
Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
|
|
2808
|
+
projectRoot: options.projectRoot,
|
|
2809
|
+
gitignoreStack: stack
|
|
2810
|
+
}))),
|
|
2811
|
+
Promise.all(files.map(async (f) => {
|
|
2812
|
+
const fileStat = await stat4(f);
|
|
2813
|
+
return {
|
|
2814
|
+
relPath: path13.relative(rootDirectoryPath, f).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
2815
|
+
absPath: f,
|
|
2816
|
+
mtimeMs: fileStat.mtimeMs
|
|
2817
|
+
};
|
|
2818
|
+
}))
|
|
2819
|
+
]);
|
|
2820
|
+
const result = [];
|
|
2821
|
+
for (const nested of dirResults) result.push(...nested);
|
|
2822
|
+
result.push(...fileStats);
|
|
2823
|
+
return result;
|
|
2824
|
+
}
|
|
2825
|
+
async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
2826
|
+
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
2827
|
+
const result = [];
|
|
2828
|
+
for (const mp of mappingPaths) {
|
|
2829
|
+
const absPath = path13.join(projectRoot, mp);
|
|
2830
|
+
try {
|
|
2831
|
+
const st = await stat4(absPath);
|
|
2832
|
+
if (st.isDirectory()) {
|
|
2833
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
2834
|
+
projectRoot,
|
|
2835
|
+
gitignoreStack
|
|
2836
|
+
});
|
|
2837
|
+
for (const entry of dirEntries) {
|
|
2838
|
+
result.push(path13.join(mp, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""));
|
|
2839
|
+
}
|
|
2840
|
+
} else {
|
|
2841
|
+
result.push(mp);
|
|
2842
|
+
}
|
|
2843
|
+
} catch {
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
return result;
|
|
2848
|
+
}
|
|
2915
2849
|
|
|
2916
2850
|
// src/formatters/message-builder.ts
|
|
2917
2851
|
function buildIssueMessage(msg) {
|
|
@@ -2972,10 +2906,11 @@ async function validate(graph, scope = "all") {
|
|
|
2972
2906
|
issues.push(...checkOrphanedAspects(graph));
|
|
2973
2907
|
let filtered = issues;
|
|
2974
2908
|
let nodesScanned = graph.nodes.size;
|
|
2975
|
-
|
|
2976
|
-
|
|
2909
|
+
const normalizedScope = scope.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2910
|
+
if (normalizedScope !== "all" && normalizedScope) {
|
|
2911
|
+
if (!graph.nodes.has(normalizedScope)) {
|
|
2977
2912
|
const parseError = (graph.nodeParseErrors ?? []).find(
|
|
2978
|
-
(e) => e.nodePath ===
|
|
2913
|
+
(e) => e.nodePath === normalizedScope || normalizedScope.startsWith(e.nodePath + "/")
|
|
2979
2914
|
);
|
|
2980
2915
|
if (parseError) {
|
|
2981
2916
|
return {
|
|
@@ -2990,13 +2925,13 @@ async function validate(graph, scope = "all") {
|
|
|
2990
2925
|
};
|
|
2991
2926
|
}
|
|
2992
2927
|
return {
|
|
2993
|
-
issues: [{ severity: "error", rule: "invalid-scope", message: buildIssueMessage({ what: `Node not found: ${
|
|
2928
|
+
issues: [{ severity: "error", rule: "invalid-scope", message: buildIssueMessage({ what: `Node not found: ${normalizedScope}`, why: "Validation scope references a node that does not exist in the graph.", next: "Check the node path and try again." }) }],
|
|
2994
2929
|
nodesScanned: 0
|
|
2995
2930
|
};
|
|
2996
2931
|
}
|
|
2997
|
-
const scopePrefix =
|
|
2998
|
-
filtered = issues.filter((i) => !i.nodePath || i.nodePath ===
|
|
2999
|
-
nodesScanned = [...graph.nodes.keys()].filter((p2) => p2 ===
|
|
2932
|
+
const scopePrefix = normalizedScope + "/";
|
|
2933
|
+
filtered = issues.filter((i) => !i.nodePath || i.nodePath === normalizedScope || i.nodePath.startsWith(scopePrefix));
|
|
2934
|
+
nodesScanned = [...graph.nodes.keys()].filter((p2) => p2 === normalizedScope || p2.startsWith(scopePrefix)).length;
|
|
3000
2935
|
}
|
|
3001
2936
|
return { issues: filtered, nodesScanned };
|
|
3002
2937
|
}
|
|
@@ -3313,12 +3248,12 @@ function checkMappingOverlap(graph) {
|
|
|
3313
3248
|
}
|
|
3314
3249
|
async function checkMappingPathsExist(graph) {
|
|
3315
3250
|
const issues = [];
|
|
3316
|
-
const projectRoot =
|
|
3251
|
+
const projectRoot = path14.dirname(graph.rootPath);
|
|
3317
3252
|
const { access: access3 } = await import("fs/promises");
|
|
3318
3253
|
for (const [nodePath, node] of graph.nodes) {
|
|
3319
3254
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
3320
3255
|
for (const mp of mappingPaths) {
|
|
3321
|
-
const absPath =
|
|
3256
|
+
const absPath = path14.join(projectRoot, mp);
|
|
3322
3257
|
try {
|
|
3323
3258
|
await access3(absPath);
|
|
3324
3259
|
} catch {
|
|
@@ -3362,13 +3297,13 @@ function checkBrokenFlowRefs(graph) {
|
|
|
3362
3297
|
async function checkWideNodes(graph) {
|
|
3363
3298
|
const issues = [];
|
|
3364
3299
|
const maxFiles = graph.config.quality?.max_mapping_source_files ?? 10;
|
|
3365
|
-
const projectRoot =
|
|
3300
|
+
const projectRoot = path14.dirname(graph.rootPath);
|
|
3366
3301
|
for (const [nodePath, node] of graph.nodes) {
|
|
3367
3302
|
const effectiveAspects = computeEffectiveAspects(node, graph);
|
|
3368
3303
|
if (effectiveAspects.size === 0) continue;
|
|
3369
3304
|
const mappingPaths = normalizeMappingPaths(node.meta.mapping);
|
|
3370
3305
|
if (mappingPaths.length === 0) continue;
|
|
3371
|
-
const sourceFiles = await
|
|
3306
|
+
const sourceFiles = await expandMappingPaths(projectRoot, mappingPaths);
|
|
3372
3307
|
if (sourceFiles.length <= maxFiles) continue;
|
|
3373
3308
|
issues.push({
|
|
3374
3309
|
severity: "warning",
|
|
@@ -3483,9 +3418,9 @@ function checkSchemas(graph) {
|
|
|
3483
3418
|
}
|
|
3484
3419
|
async function checkDirectoriesHaveNodeYaml(graph) {
|
|
3485
3420
|
const issues = [];
|
|
3486
|
-
const modelDir =
|
|
3421
|
+
const modelDir = path14.join(graph.rootPath, "model");
|
|
3487
3422
|
async function scanDir(dirPath, segments) {
|
|
3488
|
-
const entries = (await
|
|
3423
|
+
const entries = (await readdir6(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3489
3424
|
const hasNodeYaml = entries.some((e) => e.isFile() && e.name === "yg-node.yaml");
|
|
3490
3425
|
const hasFiles = entries.some((e) => e.isFile());
|
|
3491
3426
|
const graphPath = segments.join("/");
|
|
@@ -3507,47 +3442,20 @@ async function checkDirectoriesHaveNodeYaml(graph) {
|
|
|
3507
3442
|
for (const entry of entries) {
|
|
3508
3443
|
if (!entry.isDirectory()) continue;
|
|
3509
3444
|
if (entry.name.startsWith(".")) continue;
|
|
3510
|
-
await scanDir(
|
|
3445
|
+
await scanDir(path14.join(dirPath, entry.name), [...segments, entry.name]);
|
|
3511
3446
|
}
|
|
3512
3447
|
}
|
|
3513
3448
|
try {
|
|
3514
|
-
const rootEntries = (await
|
|
3449
|
+
const rootEntries = (await readdir6(modelDir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
3515
3450
|
for (const entry of rootEntries) {
|
|
3516
3451
|
if (!entry.isDirectory()) continue;
|
|
3517
3452
|
if (entry.name.startsWith(".")) continue;
|
|
3518
|
-
await scanDir(
|
|
3453
|
+
await scanDir(path14.join(modelDir, entry.name), [entry.name]);
|
|
3519
3454
|
}
|
|
3520
3455
|
} catch {
|
|
3521
3456
|
}
|
|
3522
3457
|
return issues;
|
|
3523
3458
|
}
|
|
3524
|
-
async function expandMappingToFiles(projectRoot, mappingPaths) {
|
|
3525
|
-
const files = [];
|
|
3526
|
-
async function collectFiles(absPath) {
|
|
3527
|
-
try {
|
|
3528
|
-
const s = await stat4(absPath);
|
|
3529
|
-
if (s.isFile()) {
|
|
3530
|
-
files.push(absPath);
|
|
3531
|
-
} else if (s.isDirectory()) {
|
|
3532
|
-
const entries = await readdir5(absPath, { withFileTypes: true });
|
|
3533
|
-
for (const entry of entries) {
|
|
3534
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3535
|
-
const entryPath = path13.join(absPath, entry.name);
|
|
3536
|
-
if (entry.isFile()) {
|
|
3537
|
-
files.push(entryPath);
|
|
3538
|
-
} else if (entry.isDirectory()) {
|
|
3539
|
-
await collectFiles(entryPath);
|
|
3540
|
-
}
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
} catch {
|
|
3544
|
-
}
|
|
3545
|
-
}
|
|
3546
|
-
for (const mp of mappingPaths) {
|
|
3547
|
-
await collectFiles(path13.join(projectRoot, mp));
|
|
3548
|
-
}
|
|
3549
|
-
return files;
|
|
3550
|
-
}
|
|
3551
3459
|
function checkMissingDescriptions(graph) {
|
|
3552
3460
|
const issues = [];
|
|
3553
3461
|
for (const [nodePath, node] of graph.nodes) {
|
|
@@ -3815,7 +3723,7 @@ function checkOrphanedAspects(graph) {
|
|
|
3815
3723
|
}
|
|
3816
3724
|
|
|
3817
3725
|
// src/cli/owner.ts
|
|
3818
|
-
import
|
|
3726
|
+
import path15 from "path";
|
|
3819
3727
|
import { access } from "fs/promises";
|
|
3820
3728
|
import chalk2 from "chalk";
|
|
3821
3729
|
function normalizeForMatch(inputPath) {
|
|
@@ -3846,12 +3754,10 @@ function registerOwnerCommand(program2) {
|
|
|
3846
3754
|
const graph = await loadGraph(cwd);
|
|
3847
3755
|
initDebugLog(graph.rootPath, graph.config.debug ?? false);
|
|
3848
3756
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
3849
|
-
const
|
|
3850
|
-
const absolute = path14.resolve(cwd, rawPath);
|
|
3851
|
-
const repoRelative = path14.relative(repoRoot, absolute).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
3757
|
+
const repoRelative = resolveFileArg(cwd, repoRoot, options.file);
|
|
3852
3758
|
const result = findOwner(graph, repoRoot, repoRelative);
|
|
3853
3759
|
if (!result.nodePath) {
|
|
3854
|
-
const absPath =
|
|
3760
|
+
const absPath = path15.resolve(repoRoot, result.file);
|
|
3855
3761
|
let exists = true;
|
|
3856
3762
|
try {
|
|
3857
3763
|
await access(absPath);
|
|
@@ -3891,158 +3797,6 @@ function registerOwnerCommand(program2) {
|
|
|
3891
3797
|
});
|
|
3892
3798
|
}
|
|
3893
3799
|
|
|
3894
|
-
// src/utils/hash.ts
|
|
3895
|
-
import { readFile as readFile14, readdir as readdir6, stat as stat5 } from "fs/promises";
|
|
3896
|
-
import path15 from "path";
|
|
3897
|
-
import { createHash } from "crypto";
|
|
3898
|
-
import { createRequire } from "module";
|
|
3899
|
-
var require2 = createRequire(import.meta.url);
|
|
3900
|
-
var ignoreFactory = require2("ignore");
|
|
3901
|
-
async function hashFile(filePath) {
|
|
3902
|
-
const content = await readFile14(filePath);
|
|
3903
|
-
return createHash("sha256").update(content).digest("hex");
|
|
3904
|
-
}
|
|
3905
|
-
async function loadRootGitignoreStack(projectRoot) {
|
|
3906
|
-
if (!projectRoot) return [];
|
|
3907
|
-
try {
|
|
3908
|
-
const content = await readFile14(path15.join(projectRoot, ".gitignore"), "utf-8");
|
|
3909
|
-
const matcher = ignoreFactory();
|
|
3910
|
-
matcher.add(content);
|
|
3911
|
-
return [{ basePath: projectRoot, matcher }];
|
|
3912
|
-
} catch {
|
|
3913
|
-
return [];
|
|
3914
|
-
}
|
|
3915
|
-
}
|
|
3916
|
-
function isIgnoredByStack(candidatePath, stack) {
|
|
3917
|
-
for (const { basePath, matcher } of stack) {
|
|
3918
|
-
const relativePath = path15.relative(basePath, candidatePath);
|
|
3919
|
-
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
3920
|
-
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
3921
|
-
}
|
|
3922
|
-
return false;
|
|
3923
|
-
}
|
|
3924
|
-
function hashString(content) {
|
|
3925
|
-
return createHash("sha256").update(content).digest("hex");
|
|
3926
|
-
}
|
|
3927
|
-
async function hashTrackedFiles(projectRoot, trackedFiles, storedFileData, excludePrefixes) {
|
|
3928
|
-
const fileHashes = {};
|
|
3929
|
-
const fileMtimes = {};
|
|
3930
|
-
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
3931
|
-
const allFiles = [];
|
|
3932
|
-
for (const tf of trackedFiles) {
|
|
3933
|
-
if (tf.syntheticHash) {
|
|
3934
|
-
fileHashes[tf.path] = tf.syntheticHash;
|
|
3935
|
-
continue;
|
|
3936
|
-
}
|
|
3937
|
-
const absPath = path15.join(projectRoot, tf.path);
|
|
3938
|
-
try {
|
|
3939
|
-
const st = await stat5(absPath);
|
|
3940
|
-
if (st.isDirectory()) {
|
|
3941
|
-
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
3942
|
-
projectRoot,
|
|
3943
|
-
gitignoreStack
|
|
3944
|
-
});
|
|
3945
|
-
for (const entry of dirEntries) {
|
|
3946
|
-
allFiles.push({
|
|
3947
|
-
relPath: path15.join(tf.path, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
3948
|
-
absPath: entry.absPath,
|
|
3949
|
-
mtimeMs: entry.mtimeMs
|
|
3950
|
-
});
|
|
3951
|
-
}
|
|
3952
|
-
} else {
|
|
3953
|
-
allFiles.push({ relPath: tf.path, absPath, mtimeMs: st.mtimeMs });
|
|
3954
|
-
}
|
|
3955
|
-
} catch {
|
|
3956
|
-
continue;
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
3959
|
-
const filtered = excludePrefixes?.length ? allFiles.filter((entry) => !excludePrefixes.some((prefix) => entry.relPath === prefix || entry.relPath.startsWith(prefix + "/"))) : allFiles;
|
|
3960
|
-
const dirty = [];
|
|
3961
|
-
for (const entry of filtered) {
|
|
3962
|
-
const storedMtime = storedFileData?.mtimes[entry.relPath];
|
|
3963
|
-
const storedHash = storedFileData?.hashes[entry.relPath];
|
|
3964
|
-
if (storedMtime !== void 0 && storedHash !== void 0 && entry.mtimeMs === storedMtime) {
|
|
3965
|
-
fileHashes[entry.relPath] = storedHash;
|
|
3966
|
-
} else {
|
|
3967
|
-
dirty.push(entry);
|
|
3968
|
-
}
|
|
3969
|
-
fileMtimes[entry.relPath] = entry.mtimeMs;
|
|
3970
|
-
}
|
|
3971
|
-
const BATCH_SIZE = 256;
|
|
3972
|
-
for (let i = 0; i < dirty.length; i += BATCH_SIZE) {
|
|
3973
|
-
const batch = dirty.slice(i, i + BATCH_SIZE);
|
|
3974
|
-
const hashes = await Promise.all(batch.map((e) => hashFile(e.absPath)));
|
|
3975
|
-
for (let j = 0; j < batch.length; j++) {
|
|
3976
|
-
fileHashes[batch[j].relPath] = hashes[j];
|
|
3977
|
-
}
|
|
3978
|
-
}
|
|
3979
|
-
const sorted = Object.entries(fileHashes).sort(([a], [b]) => a.localeCompare(b));
|
|
3980
|
-
const digest = sorted.map(([p2, h]) => `${p2}:${h}`).join("\n");
|
|
3981
|
-
const canonicalHash = hashString(digest);
|
|
3982
|
-
return { canonicalHash, fileHashes, fileMtimes };
|
|
3983
|
-
}
|
|
3984
|
-
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
3985
|
-
let stack = options.gitignoreStack ?? [];
|
|
3986
|
-
try {
|
|
3987
|
-
const localContent = await readFile14(path15.join(directoryPath, ".gitignore"), "utf-8");
|
|
3988
|
-
const localMatcher = ignoreFactory();
|
|
3989
|
-
localMatcher.add(localContent);
|
|
3990
|
-
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
3991
|
-
} catch {
|
|
3992
|
-
}
|
|
3993
|
-
const entries = await readdir6(directoryPath, { withFileTypes: true });
|
|
3994
|
-
const dirs = [];
|
|
3995
|
-
const files = [];
|
|
3996
|
-
for (const entry of entries) {
|
|
3997
|
-
const absoluteChildPath = path15.join(directoryPath, entry.name);
|
|
3998
|
-
if (isIgnoredByStack(absoluteChildPath, stack)) continue;
|
|
3999
|
-
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
4000
|
-
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
4001
|
-
}
|
|
4002
|
-
const [dirResults, fileStats] = await Promise.all([
|
|
4003
|
-
Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
|
|
4004
|
-
projectRoot: options.projectRoot,
|
|
4005
|
-
gitignoreStack: stack
|
|
4006
|
-
}))),
|
|
4007
|
-
Promise.all(files.map(async (f) => {
|
|
4008
|
-
const fileStat = await stat5(f);
|
|
4009
|
-
return {
|
|
4010
|
-
relPath: path15.relative(rootDirectoryPath, f).replace(/\\/g, "/").replace(/\/+$/, ""),
|
|
4011
|
-
absPath: f,
|
|
4012
|
-
mtimeMs: fileStat.mtimeMs
|
|
4013
|
-
};
|
|
4014
|
-
}))
|
|
4015
|
-
]);
|
|
4016
|
-
const result = [];
|
|
4017
|
-
for (const nested of dirResults) result.push(...nested);
|
|
4018
|
-
result.push(...fileStats);
|
|
4019
|
-
return result;
|
|
4020
|
-
}
|
|
4021
|
-
async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
4022
|
-
const gitignoreStack = await loadRootGitignoreStack(projectRoot);
|
|
4023
|
-
const result = [];
|
|
4024
|
-
for (const mp of mappingPaths) {
|
|
4025
|
-
const absPath = path15.join(projectRoot, mp);
|
|
4026
|
-
try {
|
|
4027
|
-
const st = await stat5(absPath);
|
|
4028
|
-
if (st.isDirectory()) {
|
|
4029
|
-
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
4030
|
-
projectRoot,
|
|
4031
|
-
gitignoreStack
|
|
4032
|
-
});
|
|
4033
|
-
for (const entry of dirEntries) {
|
|
4034
|
-
result.push(path15.join(mp, entry.relPath).replace(/\\/g, "/").replace(/\/+$/, ""));
|
|
4035
|
-
}
|
|
4036
|
-
} else {
|
|
4037
|
-
result.push(mp);
|
|
4038
|
-
}
|
|
4039
|
-
} catch {
|
|
4040
|
-
continue;
|
|
4041
|
-
}
|
|
4042
|
-
}
|
|
4043
|
-
return result;
|
|
4044
|
-
}
|
|
4045
|
-
|
|
4046
3800
|
// src/cli/build-context.ts
|
|
4047
3801
|
function findCandidateNodes(graph, unmappedFile) {
|
|
4048
3802
|
const dir = unmappedFile.replace(/\/[^/]+$/, "");
|
|
@@ -4108,7 +3862,8 @@ function registerBuildCommand(program2) {
|
|
|
4108
3862
|
let resolvedFilePath;
|
|
4109
3863
|
if (options.file) {
|
|
4110
3864
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
4111
|
-
const
|
|
3865
|
+
const repoRelative = resolveFileArg(process.cwd(), repoRoot, options.file);
|
|
3866
|
+
const result = findOwner(graph, repoRoot, repoRelative);
|
|
4112
3867
|
if (!result.nodePath) {
|
|
4113
3868
|
const candidates = findCandidateNodes(graph, result.file);
|
|
4114
3869
|
if (candidates.length > 0) {
|
|
@@ -4201,7 +3956,7 @@ import chalk4 from "chalk";
|
|
|
4201
3956
|
import path20 from "path";
|
|
4202
3957
|
|
|
4203
3958
|
// src/io/drift-state-store.ts
|
|
4204
|
-
import { readFile as readFile15, writeFile as writeFile5, stat as
|
|
3959
|
+
import { readFile as readFile15, writeFile as writeFile5, stat as stat5, readdir as readdir7, mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
4205
3960
|
import path16 from "path";
|
|
4206
3961
|
var DRIFT_STATE_DIR = ".drift-state";
|
|
4207
3962
|
function nodeStatePath(yggRoot, nodePath) {
|
|
@@ -4281,7 +4036,7 @@ async function readDriftState(yggRoot) {
|
|
|
4281
4036
|
const driftPath = path16.join(yggRoot, DRIFT_STATE_DIR);
|
|
4282
4037
|
let driftStat;
|
|
4283
4038
|
try {
|
|
4284
|
-
driftStat = await
|
|
4039
|
+
driftStat = await stat5(driftPath);
|
|
4285
4040
|
} catch (err) {
|
|
4286
4041
|
debugWrite(`[drift-state-store] readDriftState stat: ${err.message}`);
|
|
4287
4042
|
return {};
|
|
@@ -6238,7 +5993,8 @@ function registerImpactCommand(program2) {
|
|
|
6238
5993
|
initDebugLog(graph.rootPath, graph.config.debug ?? false);
|
|
6239
5994
|
if (options.file) {
|
|
6240
5995
|
const repoRoot = projectRootFromGraph(graph.rootPath);
|
|
6241
|
-
const
|
|
5996
|
+
const repoRelative = resolveFileArg(process.cwd(), repoRoot, options.file);
|
|
5997
|
+
const result = findOwner(graph, repoRoot, repoRelative);
|
|
6242
5998
|
if (!result.nodePath) {
|
|
6243
5999
|
process.stderr.write(chalk6.red(`${result.file} -> no graph coverage
|
|
6244
6000
|
`));
|