@chrisdudek/yg 5.0.0-alpha.3 → 5.0.0-alpha.5
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/ast.d.ts +7 -2
- package/dist/ast.js +7 -2
- package/dist/bin.js +12160 -13596
- package/dist/structure.d.ts +40 -18
- package/dist/structure.js +399 -88
- package/graph-schemas/yg-architecture.yaml +9 -4
- package/graph-schemas/yg-aspect.yaml +54 -0
- package/graph-schemas/yg-config.yaml +7 -5
- package/graph-schemas/yg-node.yaml +0 -6
- package/package.json +1 -1
package/dist/structure.js
CHANGED
|
@@ -28,9 +28,23 @@ import * as fs from "fs";
|
|
|
28
28
|
import * as path2 from "path";
|
|
29
29
|
|
|
30
30
|
// src/utils/mapping-path.ts
|
|
31
|
+
import { minimatch } from "minimatch";
|
|
31
32
|
function normalizeMappingPath(p) {
|
|
32
33
|
return p.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
33
34
|
}
|
|
35
|
+
function isGlobPattern(entry) {
|
|
36
|
+
return entry.includes("*");
|
|
37
|
+
}
|
|
38
|
+
function globMatch(file, pattern) {
|
|
39
|
+
return minimatch(file, pattern, { dot: true });
|
|
40
|
+
}
|
|
41
|
+
function mappingEntryMatchesFile(entry, file) {
|
|
42
|
+
const e = normalizeMappingPath(entry);
|
|
43
|
+
const f = normalizeMappingPath(file);
|
|
44
|
+
if (e === "") return false;
|
|
45
|
+
if (isGlobPattern(e)) return globMatch(f, e);
|
|
46
|
+
return f === e || f.startsWith(e + "/");
|
|
47
|
+
}
|
|
34
48
|
|
|
35
49
|
// src/utils/posix.ts
|
|
36
50
|
function toPosix(p) {
|
|
@@ -52,9 +66,8 @@ function isAllowed(p, set) {
|
|
|
52
66
|
if (p === "") return false;
|
|
53
67
|
if (set.has(p)) return true;
|
|
54
68
|
for (const a of set) {
|
|
55
|
-
if (a === p) return true;
|
|
56
69
|
if (a.startsWith(p + "/")) return true;
|
|
57
|
-
if (
|
|
70
|
+
if (mappingEntryMatchesFile(a, p)) return true;
|
|
58
71
|
}
|
|
59
72
|
return false;
|
|
60
73
|
}
|
|
@@ -93,36 +106,64 @@ function resolveAllowedReadPath(raw, allowedSet, projectRoot) {
|
|
|
93
106
|
return rel;
|
|
94
107
|
}
|
|
95
108
|
function createCtxFs(params) {
|
|
96
|
-
const { allowedSet, projectRoot, touchedFiles } = params;
|
|
109
|
+
const { allowedSet, projectRoot, touchedFiles, recorder, subjectFiles } = params;
|
|
97
110
|
function assertAllowed(raw) {
|
|
98
111
|
const p = resolveAllowedReadPath(raw, allowedSet, projectRoot);
|
|
99
112
|
touchedFiles.push(p);
|
|
100
113
|
return p;
|
|
101
114
|
}
|
|
115
|
+
function isSubjectFile(p) {
|
|
116
|
+
return subjectFiles !== void 0 && subjectFiles.has(p);
|
|
117
|
+
}
|
|
102
118
|
return {
|
|
103
119
|
exists(raw) {
|
|
104
120
|
const p = assertAllowed(raw);
|
|
105
121
|
const abs = path2.resolve(projectRoot, p);
|
|
122
|
+
let result;
|
|
106
123
|
try {
|
|
107
124
|
const stat2 = fs.statSync(abs);
|
|
108
|
-
|
|
125
|
+
result = stat2.isDirectory() ? "dir" : stat2.isFile() ? "file" : false;
|
|
109
126
|
} catch {
|
|
110
|
-
|
|
127
|
+
result = false;
|
|
128
|
+
}
|
|
129
|
+
if (recorder && !isSubjectFile(p)) {
|
|
130
|
+
recorder.recordExists(p, result);
|
|
111
131
|
}
|
|
132
|
+
return result;
|
|
112
133
|
},
|
|
113
134
|
read(raw) {
|
|
114
135
|
const p = assertAllowed(raw);
|
|
115
136
|
const abs = path2.resolve(projectRoot, p);
|
|
116
|
-
|
|
137
|
+
let bytes;
|
|
138
|
+
try {
|
|
139
|
+
bytes = fs.readFileSync(abs);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
if (recorder && !isSubjectFile(p)) recorder.recordReadAbsent(p);
|
|
142
|
+
throw err;
|
|
143
|
+
}
|
|
144
|
+
if (recorder && !isSubjectFile(p)) {
|
|
145
|
+
recorder.recordRead(p, bytes);
|
|
146
|
+
}
|
|
147
|
+
return bytes.toString("utf8");
|
|
117
148
|
},
|
|
118
149
|
list(raw) {
|
|
119
150
|
const p = assertAllowed(raw);
|
|
120
151
|
const abs = path2.resolve(projectRoot, p);
|
|
121
|
-
|
|
122
|
-
|
|
152
|
+
let dirents;
|
|
153
|
+
try {
|
|
154
|
+
dirents = fs.readdirSync(abs, { withFileTypes: true });
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (recorder && !isSubjectFile(p)) recorder.recordListAbsent(p);
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
const entries = dirents.map((e) => ({
|
|
123
160
|
name: e.name,
|
|
124
161
|
kind: e.isDirectory() ? "dir" : "file"
|
|
125
162
|
}));
|
|
163
|
+
if (recorder && !isSubjectFile(p)) {
|
|
164
|
+
recorder.recordList(p, entries);
|
|
165
|
+
}
|
|
166
|
+
return entries;
|
|
126
167
|
}
|
|
127
168
|
};
|
|
128
169
|
}
|
|
@@ -135,13 +176,7 @@ import * as path3 from "path";
|
|
|
135
176
|
function isPathInMapping(candidate, mapping) {
|
|
136
177
|
const c = normalizeMappingPath(candidate);
|
|
137
178
|
if (c === "") return false;
|
|
138
|
-
|
|
139
|
-
const n = normalizeMappingPath(raw);
|
|
140
|
-
if (n === "") continue;
|
|
141
|
-
if (c === n) return true;
|
|
142
|
-
if (c.startsWith(n + "/")) return true;
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
179
|
+
return mapping.some((raw) => mappingEntryMatchesFile(raw, c));
|
|
145
180
|
}
|
|
146
181
|
|
|
147
182
|
// src/structure/ctx-graph.ts
|
|
@@ -181,27 +216,48 @@ function computeAllowedNodePaths(currentPath, graph) {
|
|
|
181
216
|
return allowed;
|
|
182
217
|
}
|
|
183
218
|
function createCtxGraph(params) {
|
|
184
|
-
const { currentNodePath, graph, projectRoot, touchedFiles } = params;
|
|
219
|
+
const { currentNodePath, graph, projectRoot, touchedFiles, expandedFilesByNode, recorder, subjectFiles } = params;
|
|
185
220
|
const allowed = computeAllowedNodePaths(currentNodePath, graph);
|
|
186
221
|
function assertAllowed(id) {
|
|
187
222
|
if (!allowed.has(id)) throw new UndeclaredGraphReadError(id);
|
|
188
223
|
}
|
|
224
|
+
function recordGraphNode(m) {
|
|
225
|
+
if (!recorder) return;
|
|
226
|
+
const ygNodePath = path3.join(projectRoot, ".yggdrasil", "model", m.path, "yg-node.yaml");
|
|
227
|
+
let yamlBytes;
|
|
228
|
+
try {
|
|
229
|
+
yamlBytes = fs2.readFileSync(ygNodePath);
|
|
230
|
+
} catch {
|
|
231
|
+
yamlBytes = m.nodeYamlRaw !== void 0 ? Buffer.from(m.nodeYamlRaw, "utf8") : void 0;
|
|
232
|
+
}
|
|
233
|
+
if (yamlBytes === void 0) {
|
|
234
|
+
recorder.recordGraphNodeAbsent(m.path);
|
|
235
|
+
} else {
|
|
236
|
+
recorder.recordGraphNode(m.path, yamlBytes);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
189
239
|
function toPublicNode(m) {
|
|
190
240
|
const files = [];
|
|
191
|
-
|
|
192
|
-
|
|
241
|
+
const preExpanded = expandedFilesByNode?.get(m.path);
|
|
242
|
+
const candidatePaths = preExpanded ?? (m.meta.mapping ?? []).map(normalizeMappingPath);
|
|
243
|
+
for (const p of candidatePaths) {
|
|
193
244
|
if (!p) continue;
|
|
194
245
|
const abs = path3.resolve(projectRoot, p);
|
|
195
246
|
try {
|
|
196
247
|
const stat2 = fs2.statSync(abs);
|
|
197
248
|
if (stat2.isFile()) {
|
|
198
|
-
const
|
|
249
|
+
const bytes = fs2.readFileSync(abs);
|
|
250
|
+
const content = bytes.toString("utf8");
|
|
199
251
|
files.push({ path: p, content });
|
|
200
252
|
touchedFiles.push(p);
|
|
253
|
+
if (recorder && !subjectFiles?.has(p)) {
|
|
254
|
+
recorder.recordRead(p, bytes);
|
|
255
|
+
}
|
|
201
256
|
}
|
|
202
257
|
} catch {
|
|
203
258
|
}
|
|
204
259
|
}
|
|
260
|
+
recordGraphNode(m);
|
|
205
261
|
return {
|
|
206
262
|
id: m.path,
|
|
207
263
|
type: m.meta.type,
|
|
@@ -214,19 +270,29 @@ function createCtxGraph(params) {
|
|
|
214
270
|
node(id) {
|
|
215
271
|
assertAllowed(id);
|
|
216
272
|
const m = graph.nodes.get(id);
|
|
217
|
-
|
|
273
|
+
if (!m) {
|
|
274
|
+
if (recorder) recorder.recordGraphNodeAbsent(id);
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
return toPublicNode(m);
|
|
218
278
|
},
|
|
219
279
|
nodesByType(type) {
|
|
220
280
|
const out = [];
|
|
281
|
+
const matchedIds = [];
|
|
221
282
|
for (const id of allowed) {
|
|
222
283
|
const m = graph.nodes.get(id);
|
|
223
|
-
if (m && m.meta.type === type)
|
|
284
|
+
if (m && m.meta.type === type) {
|
|
285
|
+
matchedIds.push(m.path);
|
|
286
|
+
out.push(toPublicNode(m));
|
|
287
|
+
}
|
|
224
288
|
}
|
|
289
|
+
if (recorder) recorder.recordGraphNodesByType(type, matchedIds);
|
|
225
290
|
return out;
|
|
226
291
|
},
|
|
227
292
|
relationsFrom(node) {
|
|
228
293
|
assertAllowed(node.id);
|
|
229
294
|
const m = graph.nodes.get(node.id);
|
|
295
|
+
if (m) recordGraphNode(m);
|
|
230
296
|
return m?.meta.relations ?? [];
|
|
231
297
|
},
|
|
232
298
|
relationsTo(node) {
|
|
@@ -234,6 +300,7 @@ function createCtxGraph(params) {
|
|
|
234
300
|
for (const id of allowed) {
|
|
235
301
|
const m = graph.nodes.get(id);
|
|
236
302
|
if (!m) continue;
|
|
303
|
+
recordGraphNode(m);
|
|
237
304
|
for (const rel of m.meta.relations ?? []) {
|
|
238
305
|
if (rel.target === node.id) out.push(rel);
|
|
239
306
|
}
|
|
@@ -243,6 +310,8 @@ function createCtxGraph(params) {
|
|
|
243
310
|
children(node) {
|
|
244
311
|
assertAllowed(node.id);
|
|
245
312
|
const m = graph.nodes.get(node.id);
|
|
313
|
+
const childIds = m ? m.children.map((c) => c.path) : [];
|
|
314
|
+
if (recorder) recorder.recordGraphChildren(node.id, childIds);
|
|
246
315
|
return m ? m.children.map(toPublicNode) : [];
|
|
247
316
|
},
|
|
248
317
|
flowParticipants(flowName) {
|
|
@@ -258,6 +327,7 @@ function createCtxGraph(params) {
|
|
|
258
327
|
return false;
|
|
259
328
|
});
|
|
260
329
|
if (!participates) throw new UndeclaredGraphReadError(`flow:${flowName}`);
|
|
330
|
+
if (recorder) recorder.recordFlowParticipants(flow.name, [...flow.nodes]);
|
|
261
331
|
const out = [];
|
|
262
332
|
for (const nodeId of flow.nodes) {
|
|
263
333
|
const m = graph.nodes.get(nodeId);
|
|
@@ -500,12 +570,16 @@ var GRAMMAR_DIRS = [
|
|
|
500
570
|
path4.resolve(__dirname2, "grammars"),
|
|
501
571
|
path4.resolve(__dirname2, "..", "grammars")
|
|
502
572
|
];
|
|
503
|
-
var
|
|
573
|
+
var initPromise = null;
|
|
504
574
|
var langCache = /* @__PURE__ */ new Map();
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
575
|
+
function init() {
|
|
576
|
+
if (initPromise === null) {
|
|
577
|
+
initPromise = Parser.init();
|
|
578
|
+
initPromise.catch(() => {
|
|
579
|
+
initPromise = null;
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
return initPromise;
|
|
509
583
|
}
|
|
510
584
|
function resolveWasm(filename, pkg) {
|
|
511
585
|
for (const dir of GRAMMAR_DIRS) {
|
|
@@ -528,12 +602,16 @@ async function getParser(extension) {
|
|
|
528
602
|
throw new Error(`no parser for extension '${extension}'`);
|
|
529
603
|
}
|
|
530
604
|
const cacheKey = info.wasmFile;
|
|
531
|
-
let
|
|
532
|
-
if (
|
|
605
|
+
let langP = langCache.get(cacheKey);
|
|
606
|
+
if (langP === void 0) {
|
|
533
607
|
const wasmPath = resolveWasm(info.wasmFile, info.wasmPackage);
|
|
534
|
-
|
|
535
|
-
langCache.set(cacheKey,
|
|
608
|
+
langP = Language.load(wasmPath);
|
|
609
|
+
langCache.set(cacheKey, langP);
|
|
610
|
+
langP.catch(() => {
|
|
611
|
+
if (langCache.get(cacheKey) === langP) langCache.delete(cacheKey);
|
|
612
|
+
});
|
|
536
613
|
}
|
|
614
|
+
const lang = await langP;
|
|
537
615
|
const parser = new Parser();
|
|
538
616
|
parser.setLanguage(lang);
|
|
539
617
|
return parser;
|
|
@@ -559,7 +637,7 @@ var ParseAstNotPrewarmedError = class extends Error {
|
|
|
559
637
|
}
|
|
560
638
|
};
|
|
561
639
|
function createCtxParsers(params) {
|
|
562
|
-
const { allowedSet, projectRoot, touchedFiles, astCache } = params;
|
|
640
|
+
const { allowedSet, projectRoot, touchedFiles, astCache, recorder, subjectFiles } = params;
|
|
563
641
|
function asFile(input) {
|
|
564
642
|
if (typeof input !== "string") {
|
|
565
643
|
touchedFiles.push(input.path);
|
|
@@ -567,8 +645,18 @@ function createCtxParsers(params) {
|
|
|
567
645
|
}
|
|
568
646
|
const p = resolveAllowedReadPath(input, allowedSet, projectRoot);
|
|
569
647
|
const abs = path5.resolve(projectRoot, p);
|
|
570
|
-
|
|
648
|
+
let bytes;
|
|
649
|
+
try {
|
|
650
|
+
bytes = fs3.readFileSync(abs);
|
|
651
|
+
} catch (err) {
|
|
652
|
+
if (recorder && !subjectFiles?.has(p)) recorder.recordReadAbsent(p);
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
const content = bytes.toString("utf8");
|
|
571
656
|
touchedFiles.push(p);
|
|
657
|
+
if (recorder && !subjectFiles?.has(p)) {
|
|
658
|
+
recorder.recordRead(p, bytes);
|
|
659
|
+
}
|
|
572
660
|
return { path: p, content };
|
|
573
661
|
}
|
|
574
662
|
return {
|
|
@@ -691,7 +779,7 @@ import { createRequire as createRequire3 } from "module";
|
|
|
691
779
|
|
|
692
780
|
// src/io/repo-scanner.ts
|
|
693
781
|
import { readFile, readdir } from "fs/promises";
|
|
694
|
-
import { join, relative as relative2, sep } from "path";
|
|
782
|
+
import { join as join2, relative as relative2, sep } from "path";
|
|
695
783
|
import { createRequire as createRequire2 } from "module";
|
|
696
784
|
|
|
697
785
|
// src/utils/debug-log.ts
|
|
@@ -726,7 +814,9 @@ function isIgnoredByStack2(candidatePath, stack) {
|
|
|
726
814
|
function hashString(content) {
|
|
727
815
|
return createHash("sha256").update(content).digest("hex");
|
|
728
816
|
}
|
|
729
|
-
|
|
817
|
+
function hashBytes(bytes) {
|
|
818
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
819
|
+
}
|
|
730
820
|
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
731
821
|
let stack = options.gitignoreStack ?? [];
|
|
732
822
|
try {
|
|
@@ -764,26 +854,50 @@ async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, optio
|
|
|
764
854
|
result.push(...fileStats);
|
|
765
855
|
return result;
|
|
766
856
|
}
|
|
857
|
+
async function expandGlobEntry(projectRoot, glob, gitignoreStack) {
|
|
858
|
+
const segments = glob.split("/");
|
|
859
|
+
const firstGlobIdx = segments.findIndex((s) => isGlobPattern(s));
|
|
860
|
+
const baseSegments = firstGlobIdx > 0 ? segments.slice(0, firstGlobIdx) : [];
|
|
861
|
+
const baseDir = baseSegments.length > 0 ? path7.join(projectRoot, ...baseSegments) : projectRoot;
|
|
862
|
+
try {
|
|
863
|
+
const dirEntries = await collectDirectoryFilePaths(baseDir, projectRoot, {
|
|
864
|
+
projectRoot,
|
|
865
|
+
gitignoreStack
|
|
866
|
+
});
|
|
867
|
+
return dirEntries.filter((entry) => globMatch(entry.relPath, glob)).map((entry) => ({
|
|
868
|
+
relPath: toPosixPath(entry.relPath),
|
|
869
|
+
absPath: entry.absPath,
|
|
870
|
+
mtimeMs: entry.mtimeMs
|
|
871
|
+
}));
|
|
872
|
+
} catch {
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
}
|
|
767
876
|
async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
768
877
|
const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
|
|
769
878
|
const result = [];
|
|
770
879
|
for (const mp of mappingPaths) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
880
|
+
if (isGlobPattern(mp)) {
|
|
881
|
+
const entries = await expandGlobEntry(projectRoot, mp, gitignoreStack);
|
|
882
|
+
for (const entry of entries) result.push(entry.relPath);
|
|
883
|
+
} else {
|
|
884
|
+
const absPath = path7.join(projectRoot, mp);
|
|
885
|
+
try {
|
|
886
|
+
const st = await stat(absPath);
|
|
887
|
+
if (st.isDirectory()) {
|
|
888
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
889
|
+
projectRoot,
|
|
890
|
+
gitignoreStack
|
|
891
|
+
});
|
|
892
|
+
for (const entry of dirEntries) {
|
|
893
|
+
result.push(toPosixPath(path7.join(mp, entry.relPath)));
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
result.push(toPosixPath(mp));
|
|
781
897
|
}
|
|
782
|
-
}
|
|
783
|
-
|
|
898
|
+
} catch {
|
|
899
|
+
continue;
|
|
784
900
|
}
|
|
785
|
-
} catch {
|
|
786
|
-
continue;
|
|
787
901
|
}
|
|
788
902
|
}
|
|
789
903
|
return result;
|
|
@@ -831,8 +945,8 @@ var SuppressMarkerError = class extends Error {
|
|
|
831
945
|
}
|
|
832
946
|
code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
833
947
|
};
|
|
834
|
-
var RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)
|
|
835
|
-
var RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)
|
|
948
|
+
var RE_SINGLE = /\byg-suppress\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
949
|
+
var RE_DISABLE = /\byg-suppress-disable\(\s*([^)]+?)\s*\)\s*(.+)?$/m;
|
|
836
950
|
var RE_ENABLE = /\byg-suppress-enable\(\s*([^)]+?)\s*\)/;
|
|
837
951
|
function commentBody(text) {
|
|
838
952
|
if (text.startsWith("//")) return text.slice(2).trim();
|
|
@@ -866,15 +980,23 @@ function parseMarker(commentText, line, file) {
|
|
|
866
980
|
if (m) return makeMarker("single", m, line, file);
|
|
867
981
|
return null;
|
|
868
982
|
}
|
|
869
|
-
function collectSuppressions(tree, file, totalLines) {
|
|
870
|
-
|
|
871
|
-
return [];
|
|
872
|
-
}
|
|
873
|
-
const comments = findComments({ path: file, ast: tree });
|
|
983
|
+
function collectSuppressions(tree, file, totalLines, content) {
|
|
984
|
+
const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
|
|
874
985
|
const markers = [];
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
-
|
|
986
|
+
if (hasGrammar && tree) {
|
|
987
|
+
const comments = findComments({ path: file, ast: tree });
|
|
988
|
+
for (const c of comments) {
|
|
989
|
+
const m = parseMarker(c.text, c.startPosition.row + 1, file);
|
|
990
|
+
if (m) markers.push(m);
|
|
991
|
+
}
|
|
992
|
+
} else if (content !== void 0) {
|
|
993
|
+
const lines = content.split("\n");
|
|
994
|
+
for (let i = 0; i < lines.length; i++) {
|
|
995
|
+
const m = parseMarker(lines[i], i + 1, file);
|
|
996
|
+
if (m) markers.push(m);
|
|
997
|
+
}
|
|
998
|
+
} else {
|
|
999
|
+
return [];
|
|
878
1000
|
}
|
|
879
1001
|
markers.sort((a, b) => a.line - b.line);
|
|
880
1002
|
const ranges = [];
|
|
@@ -974,18 +1096,7 @@ function validateCheckModuleExport(mod, opts) {
|
|
|
974
1096
|
return { ok: true };
|
|
975
1097
|
}
|
|
976
1098
|
|
|
977
|
-
// src/
|
|
978
|
-
var StructureRunnerError = class extends Error {
|
|
979
|
-
constructor(code, data) {
|
|
980
|
-
super(`${code}: ${data.what}
|
|
981
|
-
${data.why}
|
|
982
|
-
${data.next}`);
|
|
983
|
-
this.code = code;
|
|
984
|
-
this.messageData = data;
|
|
985
|
-
this.name = "StructureRunnerError";
|
|
986
|
-
}
|
|
987
|
-
messageData;
|
|
988
|
-
};
|
|
1099
|
+
// src/utils/binary-extensions.ts
|
|
989
1100
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
990
1101
|
".gif",
|
|
991
1102
|
".png",
|
|
@@ -1015,6 +1126,141 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
1015
1126
|
".wasm",
|
|
1016
1127
|
".bin"
|
|
1017
1128
|
]);
|
|
1129
|
+
|
|
1130
|
+
// src/core/pair-hash.ts
|
|
1131
|
+
function observationKey(kind, target) {
|
|
1132
|
+
return `${kind}:${target}`;
|
|
1133
|
+
}
|
|
1134
|
+
var MISSING_OBSERVATION = "missing";
|
|
1135
|
+
function hashNodeSetObservation(nodeIds) {
|
|
1136
|
+
const lines = [...nodeIds].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("\n");
|
|
1137
|
+
return hashString(lines);
|
|
1138
|
+
}
|
|
1139
|
+
function hashReadObservation(bytes) {
|
|
1140
|
+
return hashBytes(bytes);
|
|
1141
|
+
}
|
|
1142
|
+
function hashListObservation(entries) {
|
|
1143
|
+
const lines = [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((e) => `${e.name}:${e.kind}`).join("\n");
|
|
1144
|
+
return hashString(lines);
|
|
1145
|
+
}
|
|
1146
|
+
function hashExistsObservation(result) {
|
|
1147
|
+
return hashString(result === false ? "false" : result);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/structure/observations.ts
|
|
1151
|
+
var ObservationRecorder = class {
|
|
1152
|
+
_entries = /* @__PURE__ */ new Map();
|
|
1153
|
+
// key → hash (first-wins)
|
|
1154
|
+
_tainted = false;
|
|
1155
|
+
/** Record a file-read observation. `bytes` is the raw content read. */
|
|
1156
|
+
recordRead(repoRelPosixPath, bytes) {
|
|
1157
|
+
this._record(observationKey("read", repoRelPosixPath), hashReadObservation(bytes));
|
|
1158
|
+
}
|
|
1159
|
+
/** Record a directory-listing observation. */
|
|
1160
|
+
recordList(repoRelPosixDir, entries) {
|
|
1161
|
+
this._record(observationKey("list", repoRelPosixDir), hashListObservation(entries));
|
|
1162
|
+
}
|
|
1163
|
+
/** Record an existence-probe observation (including negative probes where result === false). */
|
|
1164
|
+
recordExists(repoRelPosixPath, result) {
|
|
1165
|
+
this._record(observationKey("exists", repoRelPosixPath), hashExistsObservation(result));
|
|
1166
|
+
}
|
|
1167
|
+
/** Record a graph-node observation by hashing its yg-node.yaml bytes. */
|
|
1168
|
+
recordGraphNode(nodePath, ygNodeYamlBytes) {
|
|
1169
|
+
this._record(observationKey("graph", nodePath), hashReadObservation(ygNodeYamlBytes));
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Record an ABSENT file-read observation: the check attempted a read that threw
|
|
1173
|
+
* (the path passed the allow-check but the file was missing/unreadable at read
|
|
1174
|
+
* time). Folds MISSING_OBSERVATION under the same read:<path> key the verifier
|
|
1175
|
+
* re-observes — so if the check swallowed the throw and treated the file as
|
|
1176
|
+
* absent, a later successful read of that path changes the value ⇒ unverified
|
|
1177
|
+
* (spec §3.1, over-record: a throwing access is still an observation).
|
|
1178
|
+
*/
|
|
1179
|
+
recordReadAbsent(repoRelPosixPath) {
|
|
1180
|
+
this._record(observationKey("read", repoRelPosixPath), MISSING_OBSERVATION);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Record an ABSENT directory-listing observation: the check attempted a list
|
|
1184
|
+
* that threw (path allow-checked but the dir was missing/unreadable at list
|
|
1185
|
+
* time). Folds MISSING_OBSERVATION under the list:<path> key — a later
|
|
1186
|
+
* successful listing changes the value ⇒ unverified (spec §3.1, over-record).
|
|
1187
|
+
*/
|
|
1188
|
+
recordListAbsent(repoRelPosixDir) {
|
|
1189
|
+
this._record(observationKey("list", repoRelPosixDir), MISSING_OBSERVATION);
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Record a NEGATIVE graph-node observation: the check looked up a node that
|
|
1193
|
+
* does not exist. Folds the MISSING_OBSERVATION token so the verifier's
|
|
1194
|
+
* re-observation (which reads that node's yg-node.yaml and also yields
|
|
1195
|
+
* MISSING_OBSERVATION when absent) reproduces it byte-for-byte — and creating
|
|
1196
|
+
* the node later changes the value ⇒ unverified (spec §3.1, over-record).
|
|
1197
|
+
*/
|
|
1198
|
+
recordGraphNodeAbsent(nodePath) {
|
|
1199
|
+
this._record(observationKey("graph", nodePath), MISSING_OBSERVATION);
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Record a child-set observation for `nodePath`: the SET of node ids returned
|
|
1203
|
+
* by ctx.graph.children(node). Folds membership only — adding/removing a child
|
|
1204
|
+
* invalidates; a content edit to an unchanged child rides its own graph:
|
|
1205
|
+
* observation (spec §3.1).
|
|
1206
|
+
*/
|
|
1207
|
+
recordGraphChildren(nodePath, childIds) {
|
|
1208
|
+
this._record(observationKey("graph-children", nodePath), hashNodeSetObservation(childIds));
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Record a by-type-set observation for `type`: the SET of node ids returned by
|
|
1212
|
+
* ctx.graph.nodesByType(type). Folds membership only — adding/removing a node
|
|
1213
|
+
* of that type invalidates (spec §3.1).
|
|
1214
|
+
*/
|
|
1215
|
+
recordGraphNodesByType(type, nodeIds) {
|
|
1216
|
+
this._record(observationKey("graph-bytype", type), hashNodeSetObservation(nodeIds));
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Record a flow-participant-set observation for `flowName`: the SET of declared
|
|
1220
|
+
* participant ids of the flow. Folds the flow's participant list (the flow
|
|
1221
|
+
* DEFINITION's membership) so adding/removing a participant in the flow file
|
|
1222
|
+
* invalidates the verdict, even when every still-present participant node is
|
|
1223
|
+
* unchanged (spec §3.1, flowParticipants minor).
|
|
1224
|
+
*/
|
|
1225
|
+
recordFlowParticipants(flowName, participantIds) {
|
|
1226
|
+
this._record(observationKey("graph-flow", flowName), hashNodeSetObservation(participantIds));
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Returns a sorted, deduplicated array of [observationKey, observationHash] pairs.
|
|
1230
|
+
* Re-observing the same key with a different hash sets `tainted = true` and keeps
|
|
1231
|
+
* the first hash (first-observation-wins).
|
|
1232
|
+
*/
|
|
1233
|
+
snapshot() {
|
|
1234
|
+
const result = [...this._entries.entries()];
|
|
1235
|
+
result.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1236
|
+
return result;
|
|
1237
|
+
}
|
|
1238
|
+
/** True if the same path was observed with different content during this run. */
|
|
1239
|
+
get tainted() {
|
|
1240
|
+
return this._tainted;
|
|
1241
|
+
}
|
|
1242
|
+
_record(key, hash) {
|
|
1243
|
+
const existing = this._entries.get(key);
|
|
1244
|
+
if (existing === void 0) {
|
|
1245
|
+
this._entries.set(key, hash);
|
|
1246
|
+
} else if (existing !== hash) {
|
|
1247
|
+
this._tainted = true;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// src/structure/runner.ts
|
|
1253
|
+
var StructureRunnerError = class extends Error {
|
|
1254
|
+
constructor(code, data) {
|
|
1255
|
+
super(`${code}: ${data.what}
|
|
1256
|
+
${data.why}
|
|
1257
|
+
${data.next}`);
|
|
1258
|
+
this.code = code;
|
|
1259
|
+
this.messageData = data;
|
|
1260
|
+
this.name = "StructureRunnerError";
|
|
1261
|
+
}
|
|
1262
|
+
messageData;
|
|
1263
|
+
};
|
|
1018
1264
|
async function buildOwnFiles(node, projectRoot, touchedFiles) {
|
|
1019
1265
|
const childMappingEntries = [];
|
|
1020
1266
|
for (const child of node.children) {
|
|
@@ -1030,24 +1276,43 @@ async function buildOwnFiles(node, projectRoot, touchedFiles) {
|
|
|
1030
1276
|
if (childMappingEntries.length > 0 && isPathInMapping(p, childMappingEntries)) continue;
|
|
1031
1277
|
if (BINARY_EXTENSIONS.has(path8.extname(p).toLowerCase())) continue;
|
|
1032
1278
|
const abs = path8.resolve(projectRoot, p);
|
|
1033
|
-
let
|
|
1279
|
+
let bytes;
|
|
1034
1280
|
try {
|
|
1035
|
-
|
|
1281
|
+
bytes = fs4.readFileSync(abs);
|
|
1036
1282
|
} catch {
|
|
1037
1283
|
continue;
|
|
1038
1284
|
}
|
|
1039
|
-
|
|
1285
|
+
const content = bytes.toString("utf8");
|
|
1286
|
+
result.push({ file: { path: p, content }, bytes });
|
|
1040
1287
|
touchedFiles.push(p);
|
|
1041
1288
|
}
|
|
1042
1289
|
return result;
|
|
1043
1290
|
}
|
|
1291
|
+
function wrapNonSubjectFile(f, repoRelPosixPath, bytes, recorder) {
|
|
1292
|
+
if (bytes === void 0) return f;
|
|
1293
|
+
const { content, ...rest } = f;
|
|
1294
|
+
let recorded = false;
|
|
1295
|
+
const wrapped = { ...rest };
|
|
1296
|
+
Object.defineProperty(wrapped, "content", {
|
|
1297
|
+
enumerable: true,
|
|
1298
|
+
configurable: true,
|
|
1299
|
+
get() {
|
|
1300
|
+
if (!recorded) {
|
|
1301
|
+
recorder.recordRead(repoRelPosixPath, bytes);
|
|
1302
|
+
recorded = true;
|
|
1303
|
+
}
|
|
1304
|
+
return content;
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
return wrapped;
|
|
1308
|
+
}
|
|
1044
1309
|
async function enumerateMappedFilesAsync(mappingPaths, projectRoot) {
|
|
1045
1310
|
const normalized = mappingPaths.map(normalizeMappingPath).filter((p) => p !== "");
|
|
1046
1311
|
return expandMappingPaths(projectRoot, normalized);
|
|
1047
1312
|
}
|
|
1048
1313
|
async function runStructureAspect(params) {
|
|
1049
1314
|
ensureLoaderRegistered();
|
|
1050
|
-
const { aspectDir, aspectId, nodePath, graph, projectRoot } = params;
|
|
1315
|
+
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
1051
1316
|
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
1052
1317
|
const touchedFiles = [];
|
|
1053
1318
|
const node = graph.nodes.get(nodePath);
|
|
@@ -1079,21 +1344,46 @@ async function runStructureAspect(params) {
|
|
|
1079
1344
|
}
|
|
1080
1345
|
const checkFn = mod.check;
|
|
1081
1346
|
const allowedSet = collectAllowedReadsForAspect(nodePath, graph);
|
|
1082
|
-
const
|
|
1083
|
-
const
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1347
|
+
const recorder = new ObservationRecorder();
|
|
1348
|
+
const ownFilesRaw = (node.meta.mapping ?? []).map(normalizeMappingPath).filter((p) => p !== "");
|
|
1349
|
+
const ownFilesExpanded = await expandMappingPaths(projectRoot, ownFilesRaw);
|
|
1350
|
+
const subjectFiles = subjectScope !== void 0 ? new Set(subjectScope.map(normalizeMappingPath)) : new Set(ownFilesExpanded);
|
|
1351
|
+
const ctxFs = createCtxFs({ allowedSet, projectRoot, touchedFiles, recorder, subjectFiles });
|
|
1352
|
+
const expandedFilesByNode = /* @__PURE__ */ new Map();
|
|
1353
|
+
for (const id of computeAllowedNodePaths(nodePath, graph)) {
|
|
1354
|
+
const m = graph.nodes.get(id);
|
|
1355
|
+
if (m) expandedFilesByNode.set(id, await enumerateMappedFilesAsync(m.meta.mapping ?? [], projectRoot));
|
|
1356
|
+
}
|
|
1357
|
+
const ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles, expandedFilesByNode, recorder, subjectFiles });
|
|
1358
|
+
const parsers = createCtxParsers({ allowedSet, projectRoot, touchedFiles, astCache, recorder, subjectFiles });
|
|
1359
|
+
const ownFilesWithBytes = await buildOwnFiles(node, projectRoot, touchedFiles);
|
|
1360
|
+
const ownFiles = ownFilesWithBytes.map((x) => x.file);
|
|
1361
|
+
const bytesByPath = /* @__PURE__ */ new Map();
|
|
1362
|
+
for (const x of ownFilesWithBytes) bytesByPath.set(normalizeMappingPath(x.file.path), x.bytes);
|
|
1086
1363
|
await prewarmupAstCache({ astCache, projectRoot, files: ownFiles });
|
|
1087
1364
|
const ownFilesEnriched = enrichFilesWithAst(ownFiles, astCache);
|
|
1365
|
+
let nodeFilesEnriched;
|
|
1366
|
+
let ctxFilesEnriched;
|
|
1367
|
+
if (subjectScope !== void 0) {
|
|
1368
|
+
nodeFilesEnriched = recorder !== void 0 ? ownFilesEnriched.map((f) => {
|
|
1369
|
+
const p = normalizeMappingPath(f.path);
|
|
1370
|
+
if (subjectFiles.has(p)) return f;
|
|
1371
|
+
return wrapNonSubjectFile(f, p, bytesByPath.get(p), recorder);
|
|
1372
|
+
}) : ownFilesEnriched;
|
|
1373
|
+
ctxFilesEnriched = ownFilesEnriched.filter((f) => subjectFiles.has(normalizeMappingPath(f.path)));
|
|
1374
|
+
} else {
|
|
1375
|
+
nodeFilesEnriched = ownFilesEnriched;
|
|
1376
|
+
ctxFilesEnriched = ownFilesEnriched;
|
|
1377
|
+
}
|
|
1088
1378
|
const ctx = {
|
|
1089
1379
|
node: {
|
|
1090
1380
|
id: node.path,
|
|
1091
1381
|
type: node.meta.type,
|
|
1092
1382
|
mapping: node.meta.mapping ?? [],
|
|
1093
|
-
files:
|
|
1383
|
+
files: nodeFilesEnriched,
|
|
1094
1384
|
ports: node.meta.ports ?? {}
|
|
1095
1385
|
},
|
|
1096
|
-
files:
|
|
1386
|
+
files: ctxFilesEnriched,
|
|
1097
1387
|
fs: ctxFs,
|
|
1098
1388
|
graph: ctxGraph,
|
|
1099
1389
|
parseAst: parsers.parseAst,
|
|
@@ -1127,7 +1417,9 @@ async function runStructureAspect(params) {
|
|
|
1127
1417
|
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1128
1418
|
}],
|
|
1129
1419
|
touchedFiles: [],
|
|
1130
|
-
succeeded: false
|
|
1420
|
+
succeeded: false,
|
|
1421
|
+
observations: recorder.snapshot(),
|
|
1422
|
+
observationsTainted: recorder.tainted
|
|
1131
1423
|
};
|
|
1132
1424
|
}
|
|
1133
1425
|
if (err instanceof UndeclaredGraphReadError) {
|
|
@@ -1138,7 +1430,9 @@ async function runStructureAspect(params) {
|
|
|
1138
1430
|
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1139
1431
|
}],
|
|
1140
1432
|
touchedFiles: [],
|
|
1141
|
-
succeeded: false
|
|
1433
|
+
succeeded: false,
|
|
1434
|
+
observations: recorder.snapshot(),
|
|
1435
|
+
observationsTainted: recorder.tainted
|
|
1142
1436
|
};
|
|
1143
1437
|
}
|
|
1144
1438
|
if (err instanceof ParseAstNotPrewarmedError) {
|
|
@@ -1149,14 +1443,16 @@ async function runStructureAspect(params) {
|
|
|
1149
1443
|
file: `.yggdrasil/model/${nodePath}/yg-node.yaml`
|
|
1150
1444
|
}],
|
|
1151
1445
|
touchedFiles: [],
|
|
1152
|
-
succeeded: false
|
|
1446
|
+
succeeded: false,
|
|
1447
|
+
observations: recorder.snapshot(),
|
|
1448
|
+
observationsTainted: recorder.tainted
|
|
1153
1449
|
};
|
|
1154
1450
|
}
|
|
1155
1451
|
throw new StructureRunnerError("STRUCTURE_CHECK_THROWN", {
|
|
1156
1452
|
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
1157
1453
|
why: `${err.message}
|
|
1158
1454
|
${err.stack ?? ""}`,
|
|
1159
|
-
next: `Fix the bug in check.mjs
|
|
1455
|
+
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
1160
1456
|
});
|
|
1161
1457
|
}
|
|
1162
1458
|
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
@@ -1194,12 +1490,22 @@ ${err.stack ?? ""}`,
|
|
|
1194
1490
|
}
|
|
1195
1491
|
violations.push(vv);
|
|
1196
1492
|
}
|
|
1493
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
1494
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
1495
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
1496
|
+
}
|
|
1197
1497
|
const rangesByFile = /* @__PURE__ */ new Map();
|
|
1198
1498
|
function rangesFor(filePath) {
|
|
1199
1499
|
const existing = rangesByFile.get(filePath);
|
|
1200
1500
|
if (existing !== void 0) return existing;
|
|
1201
1501
|
const cached = astCache.get(filePath);
|
|
1202
|
-
|
|
1502
|
+
let ranges;
|
|
1503
|
+
if (cached) {
|
|
1504
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
1505
|
+
} else {
|
|
1506
|
+
const content = contentByPath.get(filePath);
|
|
1507
|
+
ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
|
|
1508
|
+
}
|
|
1203
1509
|
rangesByFile.set(filePath, ranges);
|
|
1204
1510
|
return ranges;
|
|
1205
1511
|
}
|
|
@@ -1209,7 +1515,13 @@ ${err.stack ?? ""}`,
|
|
|
1209
1515
|
if (!ranges) return true;
|
|
1210
1516
|
return !isLineSuppressed(ranges, aspectId, v.line);
|
|
1211
1517
|
});
|
|
1212
|
-
return {
|
|
1518
|
+
return {
|
|
1519
|
+
violations: visible,
|
|
1520
|
+
touchedFiles,
|
|
1521
|
+
succeeded: true,
|
|
1522
|
+
observations: recorder.snapshot(),
|
|
1523
|
+
observationsTainted: recorder.tainted
|
|
1524
|
+
};
|
|
1213
1525
|
}
|
|
1214
1526
|
|
|
1215
1527
|
// src/ast/walk.ts
|
|
@@ -1242,9 +1554,8 @@ function report(file, node, message) {
|
|
|
1242
1554
|
}
|
|
1243
1555
|
|
|
1244
1556
|
// src/ast/file-path.ts
|
|
1245
|
-
import { minimatch } from "minimatch";
|
|
1246
1557
|
function inFile(file, pattern) {
|
|
1247
|
-
if ("glob" in pattern) return
|
|
1558
|
+
if ("glob" in pattern) return globMatch(file.path, pattern.glob);
|
|
1248
1559
|
if ("regex" in pattern) return pattern.regex.test(file.path);
|
|
1249
1560
|
if ("contains" in pattern) return file.path.includes(pattern.contains);
|
|
1250
1561
|
return false;
|