@chrisdudek/yg 5.4.1 → 5.5.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 +3225 -1876
- package/dist/det-worker.d.ts +2 -0
- package/dist/det-worker.js +1715 -0
- package/dist/structure.d.ts +8 -0
- package/dist/structure.js +126 -41
- package/dist/templates/portal/app.css +278 -0
- package/dist/templates/portal/js/export.js +1 -1
- package/dist/templates/portal/js/views/coverage-view.js +11 -3
- package/dist/templates/portal/shell.css +257 -0
- package/dist/templates/portal/shell.html +39 -0
- package/dist/templates/portal/tokens.css +269 -0
- package/dist/templates/portal/views-audit.css +108 -0
- package/dist/templates/portal/views-flows.css +258 -0
- package/dist/templates/portal/views-panel.css +249 -0
- package/dist/templates/portal/views-relations.css +218 -0
- package/dist/templates/portal/views-rulebook.css +279 -0
- package/dist/templates/portal/views-start.css +208 -0
- package/dist/templates/portal/views.css +340 -0
- package/package.json +6 -4
|
@@ -0,0 +1,1715 @@
|
|
|
1
|
+
// src/structure/det-worker.ts
|
|
2
|
+
import { parentPort, workerData } from "worker_threads";
|
|
3
|
+
|
|
4
|
+
// src/structure/ctx-fs.ts
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
|
|
8
|
+
// src/utils/mapping-path.ts
|
|
9
|
+
import { minimatch } from "minimatch";
|
|
10
|
+
function normalizeMappingPath(p) {
|
|
11
|
+
return p.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
12
|
+
}
|
|
13
|
+
function isGlobPattern(entry) {
|
|
14
|
+
return entry.includes("*");
|
|
15
|
+
}
|
|
16
|
+
function globMatch(file, pattern) {
|
|
17
|
+
return minimatch(file, pattern, { dot: true });
|
|
18
|
+
}
|
|
19
|
+
function mappingEntryMatchesFile(entry, file) {
|
|
20
|
+
const e = normalizeMappingPath(entry);
|
|
21
|
+
const f = normalizeMappingPath(file);
|
|
22
|
+
if (e === "") return false;
|
|
23
|
+
if (isGlobPattern(e)) return globMatch(f, e);
|
|
24
|
+
return f === e || f.startsWith(e + "/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/utils/posix.ts
|
|
28
|
+
function toPosix(p) {
|
|
29
|
+
return p.replace(/\\/g, "/");
|
|
30
|
+
}
|
|
31
|
+
function toPosixPath(p) {
|
|
32
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/structure/ctx-fs.ts
|
|
36
|
+
var UndeclaredFsReadError = class extends Error {
|
|
37
|
+
constructor(path9) {
|
|
38
|
+
super(`structure-aspect-undeclared-fs-read: ${path9}`);
|
|
39
|
+
this.path = path9;
|
|
40
|
+
this.name = "UndeclaredFsReadError";
|
|
41
|
+
}
|
|
42
|
+
path;
|
|
43
|
+
};
|
|
44
|
+
function isAllowed(p, set) {
|
|
45
|
+
if (p === "") return false;
|
|
46
|
+
if (set.has(p)) return true;
|
|
47
|
+
for (const a of set) {
|
|
48
|
+
if (a.startsWith(p + "/")) return true;
|
|
49
|
+
if (mappingEntryMatchesFile(a, p)) return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function assertRealpathContained(abs, projectRoot, rel) {
|
|
54
|
+
let realRoot;
|
|
55
|
+
try {
|
|
56
|
+
realRoot = fs.realpathSync(projectRoot);
|
|
57
|
+
} catch {
|
|
58
|
+
realRoot = projectRoot;
|
|
59
|
+
}
|
|
60
|
+
let probe = abs;
|
|
61
|
+
while (!fs.existsSync(probe)) {
|
|
62
|
+
const parent = path.dirname(probe);
|
|
63
|
+
if (parent === probe) return;
|
|
64
|
+
probe = parent;
|
|
65
|
+
}
|
|
66
|
+
let realProbe;
|
|
67
|
+
try {
|
|
68
|
+
realProbe = fs.realpathSync(probe);
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const relReal = toPosix(path.relative(realRoot, realProbe));
|
|
73
|
+
if (relReal === ".." || relReal.startsWith("../") || path.isAbsolute(relReal)) {
|
|
74
|
+
throw new UndeclaredFsReadError(rel);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function resolveAllowedReadPath(raw, allowedSet, projectRoot) {
|
|
78
|
+
const abs = path.resolve(projectRoot, normalizeMappingPath(raw));
|
|
79
|
+
const rel = toPosix(path.relative(projectRoot, abs));
|
|
80
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
81
|
+
throw new UndeclaredFsReadError(normalizeMappingPath(raw));
|
|
82
|
+
}
|
|
83
|
+
if (!isAllowed(rel, allowedSet)) throw new UndeclaredFsReadError(rel);
|
|
84
|
+
assertRealpathContained(abs, projectRoot, rel);
|
|
85
|
+
return rel;
|
|
86
|
+
}
|
|
87
|
+
function createCtxFs(params) {
|
|
88
|
+
const { allowedSet, projectRoot, touchedFiles, recorder, subjectFiles } = params;
|
|
89
|
+
function assertAllowed(raw) {
|
|
90
|
+
const p = resolveAllowedReadPath(raw, allowedSet, projectRoot);
|
|
91
|
+
touchedFiles.push(p);
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
function isSubjectFile(p) {
|
|
95
|
+
return subjectFiles !== void 0 && subjectFiles.has(p);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
exists(raw) {
|
|
99
|
+
const p = assertAllowed(raw);
|
|
100
|
+
const abs = path.resolve(projectRoot, p);
|
|
101
|
+
let result;
|
|
102
|
+
try {
|
|
103
|
+
const stat2 = fs.statSync(abs);
|
|
104
|
+
result = stat2.isDirectory() ? "dir" : stat2.isFile() ? "file" : false;
|
|
105
|
+
} catch {
|
|
106
|
+
result = false;
|
|
107
|
+
}
|
|
108
|
+
if (recorder && !isSubjectFile(p)) {
|
|
109
|
+
recorder.recordExists(p, result);
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
},
|
|
113
|
+
read(raw) {
|
|
114
|
+
const p = assertAllowed(raw);
|
|
115
|
+
const abs = path.resolve(projectRoot, p);
|
|
116
|
+
let bytes;
|
|
117
|
+
try {
|
|
118
|
+
bytes = fs.readFileSync(abs);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
if (recorder && !isSubjectFile(p)) recorder.recordReadAbsent(p);
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
if (recorder && !isSubjectFile(p)) {
|
|
124
|
+
recorder.recordRead(p, bytes);
|
|
125
|
+
}
|
|
126
|
+
return bytes.toString("utf8");
|
|
127
|
+
},
|
|
128
|
+
list(raw) {
|
|
129
|
+
const p = assertAllowed(raw);
|
|
130
|
+
const abs = path.resolve(projectRoot, p);
|
|
131
|
+
let dirents;
|
|
132
|
+
try {
|
|
133
|
+
dirents = fs.readdirSync(abs, { withFileTypes: true });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (recorder && !isSubjectFile(p)) recorder.recordListAbsent(p);
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
const entries = dirents.map((e) => ({
|
|
139
|
+
name: e.name,
|
|
140
|
+
kind: e.isDirectory() ? "dir" : "file"
|
|
141
|
+
}));
|
|
142
|
+
if (recorder && !isSubjectFile(p)) {
|
|
143
|
+
recorder.recordList(p, entries);
|
|
144
|
+
}
|
|
145
|
+
return entries;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/structure/ctx-graph.ts
|
|
151
|
+
import * as fs2 from "fs";
|
|
152
|
+
import * as path2 from "path";
|
|
153
|
+
|
|
154
|
+
// src/structure/expand-mapping-sync.ts
|
|
155
|
+
function isPathInMapping(candidate, mapping) {
|
|
156
|
+
const c = normalizeMappingPath(candidate);
|
|
157
|
+
if (c === "") return false;
|
|
158
|
+
return mapping.some((raw) => mappingEntryMatchesFile(raw, c));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/structure/ctx-graph.ts
|
|
162
|
+
var UndeclaredGraphReadError = class extends Error {
|
|
163
|
+
constructor(nodePath) {
|
|
164
|
+
super(`structure-aspect-undeclared-graph-read: ${nodePath}`);
|
|
165
|
+
this.nodePath = nodePath;
|
|
166
|
+
this.name = "UndeclaredGraphReadError";
|
|
167
|
+
}
|
|
168
|
+
nodePath;
|
|
169
|
+
};
|
|
170
|
+
function makeGraphFile(repoRelPosixPath, content, bytes, recorder, subjectFiles) {
|
|
171
|
+
if (!recorder || subjectFiles?.has(repoRelPosixPath)) {
|
|
172
|
+
return { path: repoRelPosixPath, content };
|
|
173
|
+
}
|
|
174
|
+
const rec = recorder;
|
|
175
|
+
let recorded = false;
|
|
176
|
+
const file = { path: repoRelPosixPath };
|
|
177
|
+
Object.defineProperty(file, "content", {
|
|
178
|
+
enumerable: true,
|
|
179
|
+
configurable: true,
|
|
180
|
+
get() {
|
|
181
|
+
if (!recorded) {
|
|
182
|
+
rec.recordRead(repoRelPosixPath, bytes);
|
|
183
|
+
recorded = true;
|
|
184
|
+
}
|
|
185
|
+
return content;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
return file;
|
|
189
|
+
}
|
|
190
|
+
function computeAllowedNodePaths(currentPath, graph) {
|
|
191
|
+
const allowed = /* @__PURE__ */ new Set([currentPath]);
|
|
192
|
+
const current = graph.nodes.get(currentPath);
|
|
193
|
+
if (!current) return allowed;
|
|
194
|
+
for (const rel of current.meta.relations ?? []) {
|
|
195
|
+
allowed.add(rel.target);
|
|
196
|
+
const target = graph.nodes.get(rel.target);
|
|
197
|
+
if (!target) continue;
|
|
198
|
+
const relStack = [...target.children];
|
|
199
|
+
while (relStack.length > 0) {
|
|
200
|
+
const n = relStack.pop();
|
|
201
|
+
allowed.add(n.path);
|
|
202
|
+
relStack.push(...n.children);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
let cursor = current.parent;
|
|
206
|
+
while (cursor) {
|
|
207
|
+
allowed.add(cursor.path);
|
|
208
|
+
cursor = cursor.parent;
|
|
209
|
+
}
|
|
210
|
+
const stack = [...current.children];
|
|
211
|
+
while (stack.length) {
|
|
212
|
+
const n = stack.pop();
|
|
213
|
+
allowed.add(n.path);
|
|
214
|
+
stack.push(...n.children);
|
|
215
|
+
}
|
|
216
|
+
return allowed;
|
|
217
|
+
}
|
|
218
|
+
function createCtxGraph(params) {
|
|
219
|
+
const { currentNodePath, graph, projectRoot, touchedFiles, expandedFilesByNode, recorder, subjectFiles } = params;
|
|
220
|
+
const allowed = computeAllowedNodePaths(currentNodePath, graph);
|
|
221
|
+
function assertAllowed(id) {
|
|
222
|
+
if (!allowed.has(id)) throw new UndeclaredGraphReadError(id);
|
|
223
|
+
}
|
|
224
|
+
function recordGraphNode(m) {
|
|
225
|
+
if (!recorder) return;
|
|
226
|
+
const ygNodePath = path2.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
|
+
}
|
|
239
|
+
function toPublicNode(m) {
|
|
240
|
+
const files = [];
|
|
241
|
+
const preExpanded = expandedFilesByNode?.get(m.path);
|
|
242
|
+
const candidatePaths = preExpanded ?? (m.meta.mapping ?? []).map(normalizeMappingPath);
|
|
243
|
+
for (const p of candidatePaths) {
|
|
244
|
+
if (!p) continue;
|
|
245
|
+
const abs = path2.resolve(projectRoot, p);
|
|
246
|
+
try {
|
|
247
|
+
const stat2 = fs2.statSync(abs);
|
|
248
|
+
if (stat2.isFile()) {
|
|
249
|
+
const bytes = fs2.readFileSync(abs);
|
|
250
|
+
const content = bytes.toString("utf8");
|
|
251
|
+
touchedFiles.push(p);
|
|
252
|
+
files.push(makeGraphFile(p, content, bytes, recorder, subjectFiles));
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
recordGraphNode(m);
|
|
258
|
+
return {
|
|
259
|
+
id: m.path,
|
|
260
|
+
type: m.meta.type,
|
|
261
|
+
mapping: m.meta.mapping ?? [],
|
|
262
|
+
files,
|
|
263
|
+
ports: m.meta.ports ?? {}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
node(id) {
|
|
268
|
+
const self = graph.nodes.get(currentNodePath);
|
|
269
|
+
if (self) recordGraphNode(self);
|
|
270
|
+
if (!allowed.has(id)) {
|
|
271
|
+
assertAllowed(id);
|
|
272
|
+
}
|
|
273
|
+
const m = graph.nodes.get(id);
|
|
274
|
+
if (!m) {
|
|
275
|
+
if (recorder) recorder.recordGraphNodeAbsent(id);
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
return toPublicNode(m);
|
|
279
|
+
},
|
|
280
|
+
nodesByType(type) {
|
|
281
|
+
const out = [];
|
|
282
|
+
const matchedIds = [];
|
|
283
|
+
for (const id of allowed) {
|
|
284
|
+
const m = graph.nodes.get(id);
|
|
285
|
+
if (m && m.meta.type === type) {
|
|
286
|
+
matchedIds.push(m.path);
|
|
287
|
+
out.push(toPublicNode(m));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (recorder) recorder.recordGraphNodesByType(type, matchedIds);
|
|
291
|
+
return out;
|
|
292
|
+
},
|
|
293
|
+
relationsFrom(node) {
|
|
294
|
+
assertAllowed(node.id);
|
|
295
|
+
const m = graph.nodes.get(node.id);
|
|
296
|
+
if (m) recordGraphNode(m);
|
|
297
|
+
return m?.meta.relations ?? [];
|
|
298
|
+
},
|
|
299
|
+
relationsTo(node) {
|
|
300
|
+
const out = [];
|
|
301
|
+
for (const id of allowed) {
|
|
302
|
+
const m = graph.nodes.get(id);
|
|
303
|
+
if (!m) continue;
|
|
304
|
+
recordGraphNode(m);
|
|
305
|
+
for (const rel of m.meta.relations ?? []) {
|
|
306
|
+
if (rel.target === node.id) out.push(rel);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
},
|
|
311
|
+
children(node) {
|
|
312
|
+
assertAllowed(node.id);
|
|
313
|
+
const m = graph.nodes.get(node.id);
|
|
314
|
+
const childIds = m ? m.children.map((c) => c.path) : [];
|
|
315
|
+
if (recorder) recorder.recordGraphChildren(node.id, childIds);
|
|
316
|
+
return m ? m.children.map(toPublicNode) : [];
|
|
317
|
+
},
|
|
318
|
+
flowParticipants(flowName) {
|
|
319
|
+
const flow = graph.flows.find((f) => f.name === flowName || f.path === flowName);
|
|
320
|
+
if (!flow) return [];
|
|
321
|
+
const participates = flow.nodes.some((n) => {
|
|
322
|
+
if (n === currentNodePath) return true;
|
|
323
|
+
let cursor = graph.nodes.get(currentNodePath)?.parent;
|
|
324
|
+
while (cursor) {
|
|
325
|
+
if (cursor.path === n) return true;
|
|
326
|
+
cursor = cursor.parent;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
});
|
|
330
|
+
if (!participates) throw new UndeclaredGraphReadError(`flow:${flowName}`);
|
|
331
|
+
if (recorder) recorder.recordFlowParticipants(flow.name, [...flow.nodes]);
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const nodeId of flow.nodes) {
|
|
334
|
+
const m = graph.nodes.get(nodeId);
|
|
335
|
+
if (m) out.push(toPublicNode(m));
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/structure/ctx-parsers.ts
|
|
343
|
+
import * as fs3 from "fs";
|
|
344
|
+
import * as path4 from "path";
|
|
345
|
+
import { extname } from "path";
|
|
346
|
+
import { parse as parseYaml } from "yaml";
|
|
347
|
+
import { parse as parseTomlSmol } from "smol-toml";
|
|
348
|
+
|
|
349
|
+
// src/ast/parser.ts
|
|
350
|
+
import { Parser, Language } from "web-tree-sitter";
|
|
351
|
+
import path3 from "path";
|
|
352
|
+
import { fileURLToPath } from "url";
|
|
353
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
354
|
+
import { createRequire } from "module";
|
|
355
|
+
import { createHash } from "crypto";
|
|
356
|
+
|
|
357
|
+
// src/core/graph/language-registry.ts
|
|
358
|
+
var LANGUAGES = {
|
|
359
|
+
typescript: {
|
|
360
|
+
id: "typescript",
|
|
361
|
+
extensions: [".ts"],
|
|
362
|
+
wasmFile: "tree-sitter-typescript.wasm",
|
|
363
|
+
wasmPackage: "tree-sitter-typescript",
|
|
364
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-typescript",
|
|
365
|
+
grammarCommit: "",
|
|
366
|
+
treeSitterCliVersion: "",
|
|
367
|
+
externalScanner: false,
|
|
368
|
+
commentTypes: ["comment"],
|
|
369
|
+
commentDelimiters: ["//", "/*"]
|
|
370
|
+
},
|
|
371
|
+
tsx: {
|
|
372
|
+
id: "tsx",
|
|
373
|
+
extensions: [".tsx"],
|
|
374
|
+
wasmFile: "tree-sitter-tsx.wasm",
|
|
375
|
+
wasmPackage: "tree-sitter-typescript",
|
|
376
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-typescript",
|
|
377
|
+
grammarCommit: "",
|
|
378
|
+
treeSitterCliVersion: "",
|
|
379
|
+
externalScanner: false,
|
|
380
|
+
commentTypes: ["comment"],
|
|
381
|
+
commentDelimiters: ["//", "/*"]
|
|
382
|
+
},
|
|
383
|
+
javascript: {
|
|
384
|
+
id: "javascript",
|
|
385
|
+
extensions: [".js", ".mjs", ".cjs", ".jsx"],
|
|
386
|
+
wasmFile: "tree-sitter-javascript.wasm",
|
|
387
|
+
wasmPackage: "tree-sitter-javascript",
|
|
388
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-javascript",
|
|
389
|
+
grammarCommit: "",
|
|
390
|
+
treeSitterCliVersion: "",
|
|
391
|
+
externalScanner: false,
|
|
392
|
+
commentTypes: ["comment"],
|
|
393
|
+
commentDelimiters: ["//", "/*"]
|
|
394
|
+
},
|
|
395
|
+
python: {
|
|
396
|
+
id: "python",
|
|
397
|
+
extensions: [".py"],
|
|
398
|
+
wasmFile: "tree-sitter-python.wasm",
|
|
399
|
+
wasmPackage: "tree-sitter-python",
|
|
400
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-python",
|
|
401
|
+
grammarCommit: "",
|
|
402
|
+
treeSitterCliVersion: "",
|
|
403
|
+
externalScanner: false,
|
|
404
|
+
commentTypes: ["comment"],
|
|
405
|
+
commentDelimiters: ["#"]
|
|
406
|
+
},
|
|
407
|
+
go: {
|
|
408
|
+
id: "go",
|
|
409
|
+
extensions: [".go"],
|
|
410
|
+
wasmFile: "tree-sitter-go.wasm",
|
|
411
|
+
wasmPackage: "tree-sitter-go",
|
|
412
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-go",
|
|
413
|
+
grammarCommit: "",
|
|
414
|
+
treeSitterCliVersion: "",
|
|
415
|
+
externalScanner: false,
|
|
416
|
+
commentTypes: ["comment"],
|
|
417
|
+
commentDelimiters: ["//", "/*"]
|
|
418
|
+
},
|
|
419
|
+
rust: {
|
|
420
|
+
id: "rust",
|
|
421
|
+
extensions: [".rs"],
|
|
422
|
+
wasmFile: "tree-sitter-rust.wasm",
|
|
423
|
+
wasmPackage: "tree-sitter-rust",
|
|
424
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-rust",
|
|
425
|
+
grammarCommit: "",
|
|
426
|
+
treeSitterCliVersion: "",
|
|
427
|
+
externalScanner: false,
|
|
428
|
+
commentTypes: ["line_comment", "block_comment"],
|
|
429
|
+
commentDelimiters: ["//", "/*"]
|
|
430
|
+
},
|
|
431
|
+
java: {
|
|
432
|
+
id: "java",
|
|
433
|
+
extensions: [".java"],
|
|
434
|
+
wasmFile: "tree-sitter-java.wasm",
|
|
435
|
+
wasmPackage: "tree-sitter-java",
|
|
436
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-java",
|
|
437
|
+
grammarCommit: "",
|
|
438
|
+
treeSitterCliVersion: "",
|
|
439
|
+
externalScanner: false,
|
|
440
|
+
commentTypes: ["line_comment", "block_comment"],
|
|
441
|
+
commentDelimiters: ["//", "/*"]
|
|
442
|
+
},
|
|
443
|
+
csharp: {
|
|
444
|
+
id: "csharp",
|
|
445
|
+
extensions: [".cs"],
|
|
446
|
+
wasmFile: "tree-sitter-c_sharp.wasm",
|
|
447
|
+
wasmPackage: "tree-sitter-c-sharp",
|
|
448
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-c-sharp",
|
|
449
|
+
grammarCommit: "",
|
|
450
|
+
treeSitterCliVersion: "",
|
|
451
|
+
externalScanner: false,
|
|
452
|
+
commentTypes: ["comment"],
|
|
453
|
+
commentDelimiters: ["//", "/*"]
|
|
454
|
+
},
|
|
455
|
+
c: {
|
|
456
|
+
id: "c",
|
|
457
|
+
extensions: [".c", ".h"],
|
|
458
|
+
wasmFile: "tree-sitter-c.wasm",
|
|
459
|
+
wasmPackage: "tree-sitter-c",
|
|
460
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-c",
|
|
461
|
+
grammarCommit: "",
|
|
462
|
+
treeSitterCliVersion: "",
|
|
463
|
+
externalScanner: false,
|
|
464
|
+
commentTypes: ["comment"],
|
|
465
|
+
commentDelimiters: ["//", "/*"]
|
|
466
|
+
},
|
|
467
|
+
cpp: {
|
|
468
|
+
id: "cpp",
|
|
469
|
+
extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"],
|
|
470
|
+
wasmFile: "tree-sitter-cpp.wasm",
|
|
471
|
+
wasmPackage: "tree-sitter-cpp",
|
|
472
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-cpp",
|
|
473
|
+
grammarCommit: "",
|
|
474
|
+
treeSitterCliVersion: "",
|
|
475
|
+
externalScanner: false,
|
|
476
|
+
commentTypes: ["comment"],
|
|
477
|
+
commentDelimiters: ["//", "/*"]
|
|
478
|
+
},
|
|
479
|
+
php: {
|
|
480
|
+
id: "php",
|
|
481
|
+
extensions: [".php"],
|
|
482
|
+
wasmFile: "tree-sitter-php_only.wasm",
|
|
483
|
+
wasmPackage: "tree-sitter-php",
|
|
484
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-php",
|
|
485
|
+
grammarCommit: "",
|
|
486
|
+
treeSitterCliVersion: "",
|
|
487
|
+
externalScanner: false,
|
|
488
|
+
commentTypes: ["comment"],
|
|
489
|
+
commentDelimiters: ["//", "#", "/*"]
|
|
490
|
+
},
|
|
491
|
+
ruby: {
|
|
492
|
+
id: "ruby",
|
|
493
|
+
extensions: [".rb"],
|
|
494
|
+
wasmFile: "tree-sitter-ruby.wasm",
|
|
495
|
+
wasmPackage: "tree-sitter-ruby",
|
|
496
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-ruby",
|
|
497
|
+
grammarCommit: "",
|
|
498
|
+
treeSitterCliVersion: "",
|
|
499
|
+
externalScanner: false,
|
|
500
|
+
commentTypes: ["comment"],
|
|
501
|
+
commentDelimiters: ["#"]
|
|
502
|
+
},
|
|
503
|
+
json: {
|
|
504
|
+
id: "json",
|
|
505
|
+
extensions: [".json"],
|
|
506
|
+
wasmFile: "tree-sitter-json.wasm",
|
|
507
|
+
wasmPackage: "tree-sitter-json",
|
|
508
|
+
grammarRepo: "https://github.com/tree-sitter/tree-sitter-json",
|
|
509
|
+
grammarCommit: "",
|
|
510
|
+
treeSitterCliVersion: "",
|
|
511
|
+
externalScanner: false,
|
|
512
|
+
commentTypes: [],
|
|
513
|
+
commentDelimiters: []
|
|
514
|
+
},
|
|
515
|
+
kotlin: {
|
|
516
|
+
id: "kotlin",
|
|
517
|
+
extensions: [".kt", ".kts"],
|
|
518
|
+
wasmFile: "tree-sitter-kotlin.wasm",
|
|
519
|
+
wasmPackage: "@tree-sitter-grammars/tree-sitter-kotlin",
|
|
520
|
+
grammarRepo: "https://github.com/tree-sitter-grammars/tree-sitter-kotlin",
|
|
521
|
+
grammarCommit: "",
|
|
522
|
+
treeSitterCliVersion: "",
|
|
523
|
+
externalScanner: false,
|
|
524
|
+
commentTypes: ["line_comment", "block_comment"],
|
|
525
|
+
commentDelimiters: ["//", "/*"]
|
|
526
|
+
},
|
|
527
|
+
yaml: {
|
|
528
|
+
id: "yaml",
|
|
529
|
+
extensions: [".yaml", ".yml"],
|
|
530
|
+
wasmFile: "tree-sitter-yaml.wasm",
|
|
531
|
+
wasmPackage: "@tree-sitter-grammars/tree-sitter-yaml",
|
|
532
|
+
grammarRepo: "https://github.com/tree-sitter-grammars/tree-sitter-yaml",
|
|
533
|
+
grammarCommit: "",
|
|
534
|
+
treeSitterCliVersion: "",
|
|
535
|
+
externalScanner: false,
|
|
536
|
+
commentTypes: ["comment"],
|
|
537
|
+
commentDelimiters: ["#"]
|
|
538
|
+
},
|
|
539
|
+
toml: {
|
|
540
|
+
id: "toml",
|
|
541
|
+
extensions: [".toml"],
|
|
542
|
+
wasmFile: "tree-sitter-toml.wasm",
|
|
543
|
+
wasmPackage: "@tree-sitter-grammars/tree-sitter-toml",
|
|
544
|
+
grammarRepo: "https://github.com/tree-sitter-grammars/tree-sitter-toml",
|
|
545
|
+
grammarCommit: "",
|
|
546
|
+
treeSitterCliVersion: "",
|
|
547
|
+
externalScanner: false,
|
|
548
|
+
commentTypes: ["comment"],
|
|
549
|
+
commentDelimiters: ["#"]
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
var EXTENSION_TO_LANGUAGE = Object.fromEntries(
|
|
553
|
+
Object.values(LANGUAGES).flatMap((def) => def.extensions.map((ext) => [ext, def.id]))
|
|
554
|
+
);
|
|
555
|
+
function getLanguageForExtension(ext, overrides) {
|
|
556
|
+
if (overrides && ext in overrides) return overrides[ext];
|
|
557
|
+
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
558
|
+
}
|
|
559
|
+
function getGrammarForExtension(ext) {
|
|
560
|
+
const lang = getLanguageForExtension(ext.toLowerCase());
|
|
561
|
+
if (lang === null) return null;
|
|
562
|
+
const def = LANGUAGES[lang];
|
|
563
|
+
if (!def) return null;
|
|
564
|
+
return { wasmFile: def.wasmFile, wasmPackage: def.wasmPackage };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/ast/parser.ts
|
|
568
|
+
var _require = createRequire(import.meta.url);
|
|
569
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
570
|
+
var __dirname = path3.dirname(__filename);
|
|
571
|
+
var GRAMMAR_DIRS = [
|
|
572
|
+
path3.resolve(__dirname, "grammars"),
|
|
573
|
+
path3.resolve(__dirname, "..", "grammars")
|
|
574
|
+
];
|
|
575
|
+
var initPromise = null;
|
|
576
|
+
var langCache = /* @__PURE__ */ new Map();
|
|
577
|
+
var parserCache = /* @__PURE__ */ new Map();
|
|
578
|
+
function init() {
|
|
579
|
+
if (initPromise === null) {
|
|
580
|
+
initPromise = Parser.init();
|
|
581
|
+
initPromise.catch(() => {
|
|
582
|
+
initPromise = null;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
return initPromise;
|
|
586
|
+
}
|
|
587
|
+
function resolveWasm(filename, pkg) {
|
|
588
|
+
for (const dir of GRAMMAR_DIRS) {
|
|
589
|
+
const p = path3.join(dir, filename);
|
|
590
|
+
if (existsSync2(p)) return p;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
const pkgDir = path3.dirname(_require.resolve(`${pkg}/package.json`));
|
|
594
|
+
for (const candidate of [path3.join(pkgDir, filename), path3.join(pkgDir, "bindings/node", filename)]) {
|
|
595
|
+
if (existsSync2(candidate)) return candidate;
|
|
596
|
+
}
|
|
597
|
+
} catch {
|
|
598
|
+
}
|
|
599
|
+
throw new Error(`Could not find WASM grammar ${filename} in dist/grammars/ or in the ${pkg} package.`);
|
|
600
|
+
}
|
|
601
|
+
async function getParser(extension) {
|
|
602
|
+
await init();
|
|
603
|
+
const info = getGrammarForExtension(extension);
|
|
604
|
+
if (!info) {
|
|
605
|
+
throw new Error(`no parser for extension '${extension}'`);
|
|
606
|
+
}
|
|
607
|
+
const cacheKey = info.wasmFile;
|
|
608
|
+
let langP = langCache.get(cacheKey);
|
|
609
|
+
if (langP === void 0) {
|
|
610
|
+
const wasmPath = resolveWasm(info.wasmFile, info.wasmPackage);
|
|
611
|
+
langP = Language.load(wasmPath);
|
|
612
|
+
langCache.set(cacheKey, langP);
|
|
613
|
+
langP.catch(() => {
|
|
614
|
+
if (langCache.get(cacheKey) === langP) langCache.delete(cacheKey);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
const lang = await langP;
|
|
618
|
+
const existing = parserCache.get(cacheKey);
|
|
619
|
+
if (existing) return existing;
|
|
620
|
+
const parser = new Parser();
|
|
621
|
+
parser.setLanguage(lang);
|
|
622
|
+
parserCache.set(cacheKey, parser);
|
|
623
|
+
return parser;
|
|
624
|
+
}
|
|
625
|
+
async function parseFile(filePath, content) {
|
|
626
|
+
const ext = path3.extname(filePath);
|
|
627
|
+
const parser = await getParser(ext);
|
|
628
|
+
const tree = parser.parse(content);
|
|
629
|
+
if (tree === null) {
|
|
630
|
+
throw new Error(`tree-sitter failed to parse file: ${filePath}`);
|
|
631
|
+
}
|
|
632
|
+
return tree;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/structure/ctx-parsers.ts
|
|
636
|
+
var ParseAstNotPrewarmedError = class extends Error {
|
|
637
|
+
constructor(filePath) {
|
|
638
|
+
super(
|
|
639
|
+
`structure-aspect-parseast-not-prewarmed: ${filePath}. The dispatcher did not prewarm this file. Either (i) add a declared relation to the node owning this file, or (ii) use ctx.parseYaml/Json/Toml if AST is not required.`
|
|
640
|
+
);
|
|
641
|
+
this.filePath = filePath;
|
|
642
|
+
this.name = "ParseAstNotPrewarmedError";
|
|
643
|
+
}
|
|
644
|
+
filePath;
|
|
645
|
+
};
|
|
646
|
+
function createCtxParsers(params) {
|
|
647
|
+
const { allowedSet, projectRoot, touchedFiles, astCache, recorder, subjectFiles } = params;
|
|
648
|
+
function asFile(input) {
|
|
649
|
+
if (typeof input !== "string") {
|
|
650
|
+
touchedFiles.push(input.path);
|
|
651
|
+
return input;
|
|
652
|
+
}
|
|
653
|
+
const p = resolveAllowedReadPath(input, allowedSet, projectRoot);
|
|
654
|
+
const abs = path4.resolve(projectRoot, p);
|
|
655
|
+
let bytes;
|
|
656
|
+
try {
|
|
657
|
+
bytes = fs3.readFileSync(abs);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
if (recorder && !subjectFiles?.has(p)) recorder.recordReadAbsent(p);
|
|
660
|
+
throw err;
|
|
661
|
+
}
|
|
662
|
+
const content = bytes.toString("utf8");
|
|
663
|
+
touchedFiles.push(p);
|
|
664
|
+
if (recorder && !subjectFiles?.has(p)) {
|
|
665
|
+
recorder.recordRead(p, bytes);
|
|
666
|
+
}
|
|
667
|
+
return { path: p, content };
|
|
668
|
+
}
|
|
669
|
+
return {
|
|
670
|
+
parseAst(file, language) {
|
|
671
|
+
void language;
|
|
672
|
+
const f = asFile(file);
|
|
673
|
+
const cached = astCache.get(f.path);
|
|
674
|
+
if (cached && cached.content === f.content) return cached.ast;
|
|
675
|
+
throw new ParseAstNotPrewarmedError(f.path);
|
|
676
|
+
},
|
|
677
|
+
parseYaml(file) {
|
|
678
|
+
return parseYaml(asFile(file).content);
|
|
679
|
+
},
|
|
680
|
+
parseJson(file) {
|
|
681
|
+
return JSON.parse(asFile(file).content);
|
|
682
|
+
},
|
|
683
|
+
parseToml(file) {
|
|
684
|
+
return parseTomlSmol(asFile(file).content);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
async function prewarmupAstCache(params) {
|
|
689
|
+
const { astCache, files } = params;
|
|
690
|
+
for (const f of files) {
|
|
691
|
+
if (!isAstLanguageExtension(f.path)) continue;
|
|
692
|
+
const existing = astCache.get(f.path);
|
|
693
|
+
if (existing && existing.content === f.content) continue;
|
|
694
|
+
const tree = await parseFile(f.path, f.content);
|
|
695
|
+
astCache.set(f.path, { content: f.content, ast: tree });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function isAstLanguageExtension(p) {
|
|
699
|
+
return getLanguageForExtension(extname(p).toLowerCase()) !== null;
|
|
700
|
+
}
|
|
701
|
+
function enrichFilesWithAst(files, astCache) {
|
|
702
|
+
return files.map((f) => {
|
|
703
|
+
const language = getLanguageForExtension(extname(f.path)) ?? void 0;
|
|
704
|
+
const cached = astCache.get(f.path);
|
|
705
|
+
const ast = cached && cached.content === f.content ? cached.ast : void 0;
|
|
706
|
+
return { ...f, ast, language };
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/ast/suppress.ts
|
|
711
|
+
import { extname as extname3 } from "path";
|
|
712
|
+
|
|
713
|
+
// src/ast/find-comments.ts
|
|
714
|
+
import { extname as extname2 } from "path";
|
|
715
|
+
function findComments(target) {
|
|
716
|
+
const hasAst = "ast" in target;
|
|
717
|
+
const hasRootNode = "rootNode" in target;
|
|
718
|
+
if (hasAst && hasRootNode) {
|
|
719
|
+
throw new Error("AST_FINDCOMMENTS_AMBIGUOUS_TARGET: pass either ast or rootNode, not both");
|
|
720
|
+
}
|
|
721
|
+
let language = "language" in target ? target.language : void 0;
|
|
722
|
+
if (language === void 0 && "path" in target) {
|
|
723
|
+
language = getLanguageForExtension(extname2(target.path)) ?? void 0;
|
|
724
|
+
}
|
|
725
|
+
if (language === void 0) {
|
|
726
|
+
throw new Error(
|
|
727
|
+
"AST_FINDCOMMENTS_NO_LANGUAGE: pass a SourceFile whose path has a known extension, or an explicit { language }"
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
const def = LANGUAGES[language];
|
|
731
|
+
if (def === void 0) {
|
|
732
|
+
throw new Error(`AST_FINDCOMMENTS_UNKNOWN_LANGUAGE: '${language}' not in registry`);
|
|
733
|
+
}
|
|
734
|
+
const root = hasAst ? target.ast.rootNode : target.rootNode;
|
|
735
|
+
const out = [];
|
|
736
|
+
for (const type of def.commentTypes) {
|
|
737
|
+
out.push(...root.descendantsOfType(type));
|
|
738
|
+
}
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/ast/suppress.ts
|
|
743
|
+
var SuppressMarkerError = class extends Error {
|
|
744
|
+
constructor(message, file, line) {
|
|
745
|
+
super(message);
|
|
746
|
+
this.file = file;
|
|
747
|
+
this.line = line;
|
|
748
|
+
this.name = "SuppressMarkerError";
|
|
749
|
+
const loc = `${toPosixPath(file)}:${line}`;
|
|
750
|
+
this.messageData = {
|
|
751
|
+
what: `Malformed yg-suppress marker at ${loc} \u2014 it is missing the required reason after the aspect id.`,
|
|
752
|
+
why: `A single-line or disable marker must carry a reason after its closing parenthesis; a reasonless marker cannot be honored. This is a fault in the source file's marker, not in the aspect's check.`,
|
|
753
|
+
next: `Add a reason to the marker at ${loc}, or remove it.`
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
file;
|
|
757
|
+
line;
|
|
758
|
+
code = "SUPPRESS_MARKER_MISSING_REASON";
|
|
759
|
+
/**
|
|
760
|
+
* Self-describing what/why/next for the malformed marker. A reasonless marker
|
|
761
|
+
* is a fault in the SOURCE file's marker — NOT in the aspect's check.mjs — so
|
|
762
|
+
* the deterministic runners re-surface this as its own diagnostic instead of an
|
|
763
|
+
* `aspect-check-runtime-error`. (The LLM-sizing paths build their own message
|
|
764
|
+
* from `file`/`line`; only the deterministic consumers read `messageData`.)
|
|
765
|
+
*/
|
|
766
|
+
messageData;
|
|
767
|
+
};
|
|
768
|
+
var LEADER_DELIM = String.raw`\/\/[/!]*|\/\*+|\*+|#+|--+|;+|<!--|\{-|\(\*|[!%'"]`;
|
|
769
|
+
var RE_LEADER_OPTIONAL = new RegExp(String.raw`^\s*(${LEADER_DELIM})?\s*`);
|
|
770
|
+
var RE_LEADER_REQUIRED = new RegExp(String.raw`^\s*(${LEADER_DELIM})\s*`);
|
|
771
|
+
var RE_MARKER = /^yg-suppress(-disable|-enable)?\(\s*([^)]*?)\s*\)\s*(.*)$/;
|
|
772
|
+
var RE_ASPECT_ID = /^(?:\*|[A-Za-z0-9_][A-Za-z0-9_./-]*)$/;
|
|
773
|
+
var RE_ANY_CLOSER = /\s*(?:\*+\/|-->|-\}|\*\))\s*$/;
|
|
774
|
+
var RE_CLOSER_CSTYLE = /\s*\*+\/\s*$/;
|
|
775
|
+
var RE_CLOSER_HTML = /\s*-->\s*$/;
|
|
776
|
+
var RE_CLOSER_HASKELL = /\s*-\}\s*$/;
|
|
777
|
+
var RE_CLOSER_OCAML = /\s*\*\)\s*$/;
|
|
778
|
+
function stripCloser(reason, leader) {
|
|
779
|
+
if (leader === void 0) return reason.replace(RE_ANY_CLOSER, "");
|
|
780
|
+
if (leader.startsWith("//")) return reason;
|
|
781
|
+
if (leader.startsWith("/*") || /^\*+$/.test(leader)) return reason.replace(RE_CLOSER_CSTYLE, "");
|
|
782
|
+
if (leader === "<!--") return reason.replace(RE_CLOSER_HTML, "");
|
|
783
|
+
if (leader === "{-") return reason.replace(RE_CLOSER_HASKELL, "");
|
|
784
|
+
if (leader === "(*") return reason.replace(RE_CLOSER_OCAML, "");
|
|
785
|
+
return reason;
|
|
786
|
+
}
|
|
787
|
+
function splitAspectList(raw) {
|
|
788
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
789
|
+
}
|
|
790
|
+
function matchMarkerLine(line, requireDelimiter) {
|
|
791
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
792
|
+
const leader = (requireDelimiter ? RE_LEADER_REQUIRED : RE_LEADER_OPTIONAL).exec(line);
|
|
793
|
+
if (leader === null) return null;
|
|
794
|
+
const m = RE_MARKER.exec(line.slice(leader[0].length));
|
|
795
|
+
if (m === null) return null;
|
|
796
|
+
const aspectIds = splitAspectList(m[2]);
|
|
797
|
+
if (aspectIds.length === 0) return null;
|
|
798
|
+
if (!aspectIds.every((id) => RE_ASPECT_ID.test(id))) return null;
|
|
799
|
+
const kind = m[1] === "-disable" ? "disable" : m[1] === "-enable" ? "enable" : "single";
|
|
800
|
+
const reason = kind === "enable" ? "" : stripCloser(m[3], leader[1]).trim();
|
|
801
|
+
return { kind, aspectIds, reason };
|
|
802
|
+
}
|
|
803
|
+
function parseMarker(lineText, line, file, requireDelimiter) {
|
|
804
|
+
const m = matchMarkerLine(lineText, requireDelimiter);
|
|
805
|
+
if (m === null) return null;
|
|
806
|
+
if (m.kind !== "enable" && m.reason === "") {
|
|
807
|
+
throw new SuppressMarkerError(
|
|
808
|
+
`yg-suppress${m.kind === "disable" ? "-disable" : ""}(${m.aspectIds.join(", ")}) missing reason at ${file}:${line}`,
|
|
809
|
+
file,
|
|
810
|
+
line
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
return { kind: m.kind, aspectIds: m.aspectIds, reason: m.reason, line };
|
|
814
|
+
}
|
|
815
|
+
var RE_FENCE = /^ {0,3}((?:`{3,})|(?:~{3,}))[ \t]*(.*)$/;
|
|
816
|
+
var MARKDOWN_EXTS = /* @__PURE__ */ new Set([".md", ".markdown"]);
|
|
817
|
+
function isMarkdownExt(file) {
|
|
818
|
+
return MARKDOWN_EXTS.has(extname3(file).toLowerCase());
|
|
819
|
+
}
|
|
820
|
+
function markdownFencedLines(text) {
|
|
821
|
+
const fenced = /* @__PURE__ */ new Set();
|
|
822
|
+
const lines = text.split("\n");
|
|
823
|
+
let open = null;
|
|
824
|
+
for (let i = 0; i < lines.length; i++) {
|
|
825
|
+
const m = RE_FENCE.exec(lines[i]);
|
|
826
|
+
if (open === null) {
|
|
827
|
+
if (m) {
|
|
828
|
+
open = { char: m[1][0], len: m[1].length };
|
|
829
|
+
fenced.add(i + 1);
|
|
830
|
+
}
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
fenced.add(i + 1);
|
|
834
|
+
if (m && m[1][0] === open.char && m[1].length >= open.len && m[2].trim() === "") {
|
|
835
|
+
open = null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return fenced;
|
|
839
|
+
}
|
|
840
|
+
function collectSuppressions(tree, file, totalLines, content) {
|
|
841
|
+
const hasGrammar = getLanguageForExtension(extname3(file)) !== null;
|
|
842
|
+
const markers = [];
|
|
843
|
+
if (hasGrammar && tree) {
|
|
844
|
+
const comments = findComments({ path: file, ast: tree });
|
|
845
|
+
for (const c of comments) {
|
|
846
|
+
const commentLines = c.text.split("\n");
|
|
847
|
+
for (let i = 0; i < commentLines.length; i++) {
|
|
848
|
+
const m = parseMarker(commentLines[i], c.startPosition.row + i + 1, file, false);
|
|
849
|
+
if (m) markers.push(m);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
} else if (content !== void 0) {
|
|
853
|
+
const lines = content.split("\n");
|
|
854
|
+
const fenced = isMarkdownExt(file) ? markdownFencedLines(content) : null;
|
|
855
|
+
for (let i = 0; i < lines.length; i++) {
|
|
856
|
+
if (fenced && fenced.has(i + 1)) continue;
|
|
857
|
+
const m = parseMarker(lines[i], i + 1, file, true);
|
|
858
|
+
if (m) markers.push(m);
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
markers.sort((a, b) => a.line - b.line);
|
|
864
|
+
const ranges = [];
|
|
865
|
+
const openSpecific = /* @__PURE__ */ new Map();
|
|
866
|
+
let openWildcard = null;
|
|
867
|
+
for (const m of markers) {
|
|
868
|
+
if (m.kind === "single") {
|
|
869
|
+
const isWildcard = m.aspectIds.includes("*");
|
|
870
|
+
ranges.push({ aspectIds: new Set(m.aspectIds), startLine: m.line + 1, endLine: m.line + 1, isWildcard });
|
|
871
|
+
} else if (m.kind === "disable") {
|
|
872
|
+
for (const id of m.aspectIds) {
|
|
873
|
+
if (id === "*") {
|
|
874
|
+
if (openWildcard === null) openWildcard = m.line + 1;
|
|
875
|
+
} else {
|
|
876
|
+
if (!openSpecific.has(id)) openSpecific.set(id, m.line + 1);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
} else {
|
|
880
|
+
for (const id of m.aspectIds) {
|
|
881
|
+
if (id === "*") {
|
|
882
|
+
if (openWildcard !== null) {
|
|
883
|
+
if (m.line - 1 >= openWildcard) {
|
|
884
|
+
ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: m.line - 1, isWildcard: true });
|
|
885
|
+
}
|
|
886
|
+
openWildcard = null;
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
const start = openSpecific.get(id);
|
|
890
|
+
if (start !== void 0) {
|
|
891
|
+
if (m.line - 1 >= start) {
|
|
892
|
+
ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: m.line - 1, isWildcard: false });
|
|
893
|
+
}
|
|
894
|
+
openSpecific.delete(id);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (openWildcard !== null && openWildcard <= totalLines) ranges.push({ aspectIds: /* @__PURE__ */ new Set(["*"]), startLine: openWildcard, endLine: totalLines, isWildcard: true });
|
|
901
|
+
for (const [id, start] of openSpecific) {
|
|
902
|
+
if (start <= totalLines) ranges.push({ aspectIds: /* @__PURE__ */ new Set([id]), startLine: start, endLine: totalLines, isWildcard: false });
|
|
903
|
+
}
|
|
904
|
+
return ranges;
|
|
905
|
+
}
|
|
906
|
+
function isLineSuppressed(ranges, aspectId, line) {
|
|
907
|
+
return ranges.some((r) => {
|
|
908
|
+
if (line < r.startLine || line > r.endLine) return false;
|
|
909
|
+
return r.isWildcard || r.aspectIds.has(aspectId);
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/utils/validate-check-module.ts
|
|
914
|
+
function validateCheckModuleExport(mod, opts) {
|
|
915
|
+
const { codePrefix, runnerLabel } = opts;
|
|
916
|
+
if (mod.check === void 0) {
|
|
917
|
+
const defaultExport = mod.default;
|
|
918
|
+
if (typeof defaultExport === "function" && defaultExport.name === "check") {
|
|
919
|
+
return {
|
|
920
|
+
ok: false,
|
|
921
|
+
code: `${codePrefix}_CHECK_DEFAULT_EXPORT`,
|
|
922
|
+
message: {
|
|
923
|
+
what: `check.mjs exports 'check' as default, but a NAMED export is required (${runnerLabel}).`,
|
|
924
|
+
why: `The runner imports the named export. A default export is invisible to it.`,
|
|
925
|
+
next: `Change 'export default function check(...)' to 'export function check(...)'.`
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
return {
|
|
930
|
+
ok: false,
|
|
931
|
+
code: `${codePrefix}_CHECK_NOT_EXPORTED`,
|
|
932
|
+
message: {
|
|
933
|
+
what: `check.mjs does not export a function named 'check' (${runnerLabel}).`,
|
|
934
|
+
why: `The runner expects 'export function check(ctx) { ... }'.`,
|
|
935
|
+
next: `Add a named export 'check' in check.mjs.`
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
if (typeof mod.check !== "function") {
|
|
940
|
+
return {
|
|
941
|
+
ok: false,
|
|
942
|
+
code: `${codePrefix}_CHECK_NOT_FUNCTION`,
|
|
943
|
+
message: {
|
|
944
|
+
what: `'check' is exported but is not a function (got ${typeof mod.check}).`,
|
|
945
|
+
why: `The runner calls check(ctx).`,
|
|
946
|
+
next: `Re-export check as a function.`
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
const checkFn = mod.check;
|
|
951
|
+
if (checkFn.length !== 1) {
|
|
952
|
+
return {
|
|
953
|
+
ok: false,
|
|
954
|
+
code: `${codePrefix}_CHECK_WRONG_ARITY`,
|
|
955
|
+
message: {
|
|
956
|
+
what: `'check' must accept exactly 1 parameter (ctx); declared arity is ${checkFn.length}.`,
|
|
957
|
+
why: `The runner invokes check(ctx).`,
|
|
958
|
+
next: `Change the signature to function check(ctx).`
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
return { ok: true };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/ast/parse-cache.ts
|
|
966
|
+
function destroyParseCache(cache) {
|
|
967
|
+
for (const { ast } of cache.values()) {
|
|
968
|
+
ast.delete();
|
|
969
|
+
}
|
|
970
|
+
cache.clear();
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/structure/hook-loader.ts
|
|
974
|
+
import * as fs4 from "fs";
|
|
975
|
+
import * as path8 from "path";
|
|
976
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
977
|
+
|
|
978
|
+
// src/ast/loader-hook.ts
|
|
979
|
+
import { register } from "module";
|
|
980
|
+
import { pathToFileURL } from "url";
|
|
981
|
+
import path5 from "path";
|
|
982
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
983
|
+
import { existsSync as existsSync3 } from "fs";
|
|
984
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
985
|
+
var __dirname2 = path5.dirname(__filename2);
|
|
986
|
+
var registered = false;
|
|
987
|
+
function ensureLoaderRegistered() {
|
|
988
|
+
if (registered) return;
|
|
989
|
+
let implPath = path5.resolve(__dirname2, "./loader-hook-impl.js");
|
|
990
|
+
if (!existsSync3(implPath)) {
|
|
991
|
+
const pkgRoot = path5.resolve(__dirname2, "../../");
|
|
992
|
+
implPath = path5.resolve(pkgRoot, "dist/loader-hook-impl.js");
|
|
993
|
+
}
|
|
994
|
+
register(pathToFileURL(implPath));
|
|
995
|
+
registered = true;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/structure/allowed-reads.ts
|
|
999
|
+
function collectAllowedReadsForAspect(nodePath, graph) {
|
|
1000
|
+
const allowed = /* @__PURE__ */ new Set();
|
|
1001
|
+
const node = graph.nodes.get(nodePath);
|
|
1002
|
+
if (!node) return allowed;
|
|
1003
|
+
const addMapping = (n) => {
|
|
1004
|
+
const mapping = n.meta.mapping ?? [];
|
|
1005
|
+
for (const raw of mapping) {
|
|
1006
|
+
const p = normalizeMappingPath(raw);
|
|
1007
|
+
if (p) allowed.add(p);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
const childPaths = /* @__PURE__ */ new Set();
|
|
1011
|
+
for (const child of node.children) {
|
|
1012
|
+
for (const raw of child.meta.mapping ?? []) {
|
|
1013
|
+
const p = normalizeMappingPath(raw);
|
|
1014
|
+
if (p) childPaths.add(p);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const raw of node.meta.mapping ?? []) {
|
|
1018
|
+
const p = normalizeMappingPath(raw);
|
|
1019
|
+
if (p && !childPaths.has(p)) allowed.add(p);
|
|
1020
|
+
}
|
|
1021
|
+
for (const rel of node.meta.relations ?? []) {
|
|
1022
|
+
const target = graph.nodes.get(rel.target);
|
|
1023
|
+
if (!target) continue;
|
|
1024
|
+
addMapping(target);
|
|
1025
|
+
const relStack = [...target.children];
|
|
1026
|
+
while (relStack.length > 0) {
|
|
1027
|
+
const n = relStack.pop();
|
|
1028
|
+
addMapping(n);
|
|
1029
|
+
relStack.push(...n.children);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
let cursor = node.parent;
|
|
1033
|
+
while (cursor) {
|
|
1034
|
+
addMapping(cursor);
|
|
1035
|
+
cursor = cursor.parent;
|
|
1036
|
+
}
|
|
1037
|
+
const parentDirs = /* @__PURE__ */ new Set();
|
|
1038
|
+
for (const raw of node.meta.mapping ?? []) {
|
|
1039
|
+
const p = normalizeMappingPath(raw);
|
|
1040
|
+
if (p) {
|
|
1041
|
+
const lastSlash = p.lastIndexOf("/");
|
|
1042
|
+
if (lastSlash > 0) {
|
|
1043
|
+
parentDirs.add(p.substring(0, lastSlash));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
const siblingCarveOut = /* @__PURE__ */ new Set();
|
|
1048
|
+
for (const cp of childPaths) {
|
|
1049
|
+
const lastSlash = cp.lastIndexOf("/");
|
|
1050
|
+
if (lastSlash > 0) {
|
|
1051
|
+
const cpDir = cp.substring(0, lastSlash);
|
|
1052
|
+
if (parentDirs.has(cpDir)) {
|
|
1053
|
+
siblingCarveOut.add(cp);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const stack = [...node.children];
|
|
1058
|
+
while (stack.length > 0) {
|
|
1059
|
+
const n = stack.pop();
|
|
1060
|
+
for (const raw of n.meta.mapping ?? []) {
|
|
1061
|
+
const p = normalizeMappingPath(raw);
|
|
1062
|
+
if (p && !siblingCarveOut.has(p)) allowed.add(p);
|
|
1063
|
+
}
|
|
1064
|
+
stack.push(...n.children);
|
|
1065
|
+
}
|
|
1066
|
+
return allowed;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/io/hash.ts
|
|
1070
|
+
import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
|
|
1071
|
+
import path7 from "path";
|
|
1072
|
+
import { createHash as createHash2 } from "crypto";
|
|
1073
|
+
import { createRequire as createRequire3 } from "module";
|
|
1074
|
+
|
|
1075
|
+
// src/io/repo-scanner.ts
|
|
1076
|
+
import { readFile, readdir } from "fs/promises";
|
|
1077
|
+
import { join as join2, relative as relative2, sep } from "path";
|
|
1078
|
+
import { createRequire as createRequire2 } from "module";
|
|
1079
|
+
|
|
1080
|
+
// src/utils/debug-log.ts
|
|
1081
|
+
import path6 from "path";
|
|
1082
|
+
|
|
1083
|
+
// src/io/repo-scanner.ts
|
|
1084
|
+
var require2 = createRequire2(import.meta.url);
|
|
1085
|
+
var ignoreFactory = require2("ignore");
|
|
1086
|
+
|
|
1087
|
+
// src/io/hash.ts
|
|
1088
|
+
var require3 = createRequire3(import.meta.url);
|
|
1089
|
+
var ignoreFactory2 = require3("ignore");
|
|
1090
|
+
var CR = 13;
|
|
1091
|
+
var LF = 10;
|
|
1092
|
+
function normalizeLineEndings(bytes) {
|
|
1093
|
+
if (!bytes.includes(CR)) return bytes;
|
|
1094
|
+
const out = Buffer.allocUnsafe(bytes.length);
|
|
1095
|
+
let w = 0;
|
|
1096
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1097
|
+
if (bytes[i] === CR) {
|
|
1098
|
+
out[w++] = LF;
|
|
1099
|
+
if (bytes[i + 1] === LF) i++;
|
|
1100
|
+
} else {
|
|
1101
|
+
out[w++] = bytes[i];
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return out.subarray(0, w);
|
|
1105
|
+
}
|
|
1106
|
+
async function loadRootGitignoreStack2(projectRoot) {
|
|
1107
|
+
if (!projectRoot) return [];
|
|
1108
|
+
try {
|
|
1109
|
+
const content = await readFile2(path7.join(projectRoot, ".gitignore"), "utf-8");
|
|
1110
|
+
const matcher = ignoreFactory2();
|
|
1111
|
+
matcher.add(content);
|
|
1112
|
+
return [{ basePath: projectRoot, matcher }];
|
|
1113
|
+
} catch {
|
|
1114
|
+
return [];
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function isIgnoredByStack2(candidatePath, stack) {
|
|
1118
|
+
for (const { basePath, matcher } of stack) {
|
|
1119
|
+
const relativePath = toPosix(path7.relative(basePath, candidatePath));
|
|
1120
|
+
if (relativePath === "" || relativePath.startsWith("..")) continue;
|
|
1121
|
+
if (matcher.ignores(relativePath) || matcher.ignores(relativePath + "/")) return true;
|
|
1122
|
+
}
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
function hashString(content) {
|
|
1126
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
1127
|
+
}
|
|
1128
|
+
function hashBytes(bytes) {
|
|
1129
|
+
return createHash2("sha256").update(normalizeLineEndings(bytes)).digest("hex");
|
|
1130
|
+
}
|
|
1131
|
+
async function collectDirectoryFilePaths(directoryPath, rootDirectoryPath, options) {
|
|
1132
|
+
let stack = options.gitignoreStack ?? [];
|
|
1133
|
+
try {
|
|
1134
|
+
const localContent = await readFile2(path7.join(directoryPath, ".gitignore"), "utf-8");
|
|
1135
|
+
const localMatcher = ignoreFactory2();
|
|
1136
|
+
localMatcher.add(localContent);
|
|
1137
|
+
stack = [...stack, { basePath: directoryPath, matcher: localMatcher }];
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
const entries = await readdir2(directoryPath, { withFileTypes: true });
|
|
1141
|
+
const dirs = [];
|
|
1142
|
+
const files = [];
|
|
1143
|
+
for (const entry of entries) {
|
|
1144
|
+
const absoluteChildPath = path7.join(directoryPath, entry.name);
|
|
1145
|
+
if (isIgnoredByStack2(absoluteChildPath, stack)) continue;
|
|
1146
|
+
if (entry.isDirectory()) dirs.push(absoluteChildPath);
|
|
1147
|
+
else if (entry.isFile()) files.push(absoluteChildPath);
|
|
1148
|
+
}
|
|
1149
|
+
const [dirResults, fileStats] = await Promise.all([
|
|
1150
|
+
Promise.all(dirs.map((d) => collectDirectoryFilePaths(d, rootDirectoryPath, {
|
|
1151
|
+
projectRoot: options.projectRoot,
|
|
1152
|
+
gitignoreStack: stack
|
|
1153
|
+
}))),
|
|
1154
|
+
Promise.all(files.map(async (f) => {
|
|
1155
|
+
const fileStat = await stat(f);
|
|
1156
|
+
return {
|
|
1157
|
+
relPath: toPosixPath(path7.relative(rootDirectoryPath, f)),
|
|
1158
|
+
absPath: f,
|
|
1159
|
+
mtimeMs: fileStat.mtimeMs
|
|
1160
|
+
};
|
|
1161
|
+
}))
|
|
1162
|
+
]);
|
|
1163
|
+
const result = [];
|
|
1164
|
+
for (const nested of dirResults) result.push(...nested);
|
|
1165
|
+
result.push(...fileStats);
|
|
1166
|
+
return result;
|
|
1167
|
+
}
|
|
1168
|
+
async function expandGlobEntry(projectRoot, glob, gitignoreStack) {
|
|
1169
|
+
const segments = glob.split("/");
|
|
1170
|
+
const firstGlobIdx = segments.findIndex((s) => isGlobPattern(s));
|
|
1171
|
+
const baseSegments = firstGlobIdx > 0 ? segments.slice(0, firstGlobIdx) : [];
|
|
1172
|
+
const baseDir = baseSegments.length > 0 ? path7.join(projectRoot, ...baseSegments) : projectRoot;
|
|
1173
|
+
try {
|
|
1174
|
+
const dirEntries = await collectDirectoryFilePaths(baseDir, projectRoot, {
|
|
1175
|
+
projectRoot,
|
|
1176
|
+
gitignoreStack
|
|
1177
|
+
});
|
|
1178
|
+
return dirEntries.filter((entry) => globMatch(entry.relPath, glob)).map((entry) => ({
|
|
1179
|
+
relPath: toPosixPath(entry.relPath),
|
|
1180
|
+
absPath: entry.absPath,
|
|
1181
|
+
mtimeMs: entry.mtimeMs
|
|
1182
|
+
}));
|
|
1183
|
+
} catch {
|
|
1184
|
+
return [];
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function expandMappingPaths(projectRoot, mappingPaths) {
|
|
1188
|
+
const gitignoreStack = await loadRootGitignoreStack2(projectRoot);
|
|
1189
|
+
const result = [];
|
|
1190
|
+
for (const mp of mappingPaths) {
|
|
1191
|
+
if (isGlobPattern(mp)) {
|
|
1192
|
+
const entries = await expandGlobEntry(projectRoot, mp, gitignoreStack);
|
|
1193
|
+
for (const entry of entries) result.push(entry.relPath);
|
|
1194
|
+
} else {
|
|
1195
|
+
const absPath = path7.join(projectRoot, mp);
|
|
1196
|
+
try {
|
|
1197
|
+
const st = await stat(absPath);
|
|
1198
|
+
if (st.isDirectory()) {
|
|
1199
|
+
const dirEntries = await collectDirectoryFilePaths(absPath, absPath, {
|
|
1200
|
+
projectRoot,
|
|
1201
|
+
gitignoreStack
|
|
1202
|
+
});
|
|
1203
|
+
for (const entry of dirEntries) {
|
|
1204
|
+
result.push(toPosixPath(path7.join(mp, entry.relPath)));
|
|
1205
|
+
}
|
|
1206
|
+
} else {
|
|
1207
|
+
result.push(toPosixPath(mp));
|
|
1208
|
+
}
|
|
1209
|
+
} catch {
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return result;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/utils/binary-extensions.ts
|
|
1218
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1219
|
+
".gif",
|
|
1220
|
+
".png",
|
|
1221
|
+
".jpg",
|
|
1222
|
+
".jpeg",
|
|
1223
|
+
".webp",
|
|
1224
|
+
".bmp",
|
|
1225
|
+
".ico",
|
|
1226
|
+
".svgz",
|
|
1227
|
+
".woff",
|
|
1228
|
+
".woff2",
|
|
1229
|
+
".ttf",
|
|
1230
|
+
".otf",
|
|
1231
|
+
".eot",
|
|
1232
|
+
".zip",
|
|
1233
|
+
".gz",
|
|
1234
|
+
".tgz",
|
|
1235
|
+
".tar",
|
|
1236
|
+
".bz2",
|
|
1237
|
+
".7z",
|
|
1238
|
+
".pdf",
|
|
1239
|
+
".mp4",
|
|
1240
|
+
".mov",
|
|
1241
|
+
".webm",
|
|
1242
|
+
".mp3",
|
|
1243
|
+
".wav",
|
|
1244
|
+
".wasm",
|
|
1245
|
+
".bin"
|
|
1246
|
+
]);
|
|
1247
|
+
|
|
1248
|
+
// src/core/pair-hash.ts
|
|
1249
|
+
function observationKey(kind, target) {
|
|
1250
|
+
return `${kind}:${target}`;
|
|
1251
|
+
}
|
|
1252
|
+
var MISSING_OBSERVATION = "missing";
|
|
1253
|
+
function hashNodeSetObservation(nodeIds) {
|
|
1254
|
+
const lines = [...nodeIds].sort((a, b) => a < b ? -1 : a > b ? 1 : 0).join("\n");
|
|
1255
|
+
return hashString(lines);
|
|
1256
|
+
}
|
|
1257
|
+
function hashReadObservation(bytes) {
|
|
1258
|
+
return hashBytes(bytes);
|
|
1259
|
+
}
|
|
1260
|
+
function hashListObservation(entries) {
|
|
1261
|
+
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");
|
|
1262
|
+
return hashString(lines);
|
|
1263
|
+
}
|
|
1264
|
+
function hashExistsObservation(result) {
|
|
1265
|
+
return hashString(result === false ? "false" : result);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/structure/observations.ts
|
|
1269
|
+
var ObservationRecorder = class {
|
|
1270
|
+
_entries = /* @__PURE__ */ new Map();
|
|
1271
|
+
// key → hash (first-wins)
|
|
1272
|
+
_tainted = false;
|
|
1273
|
+
/** Record a file-read observation. `bytes` is the raw content read. */
|
|
1274
|
+
recordRead(repoRelPosixPath, bytes) {
|
|
1275
|
+
this._record(observationKey("read", repoRelPosixPath), hashReadObservation(bytes));
|
|
1276
|
+
}
|
|
1277
|
+
/** Record a directory-listing observation. */
|
|
1278
|
+
recordList(repoRelPosixDir, entries) {
|
|
1279
|
+
this._record(observationKey("list", repoRelPosixDir), hashListObservation(entries));
|
|
1280
|
+
}
|
|
1281
|
+
/** Record an existence-probe observation (including negative probes where result === false). */
|
|
1282
|
+
recordExists(repoRelPosixPath, result) {
|
|
1283
|
+
this._record(observationKey("exists", repoRelPosixPath), hashExistsObservation(result));
|
|
1284
|
+
}
|
|
1285
|
+
/** Record a graph-node observation by hashing its yg-node.yaml bytes. */
|
|
1286
|
+
recordGraphNode(nodePath, ygNodeYamlBytes) {
|
|
1287
|
+
this._record(observationKey("graph", nodePath), hashReadObservation(ygNodeYamlBytes));
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Record an ABSENT file-read observation: the check attempted a read that threw
|
|
1291
|
+
* (the path passed the allow-check but the file was missing/unreadable at read
|
|
1292
|
+
* time). Folds MISSING_OBSERVATION under the same read:<path> key the verifier
|
|
1293
|
+
* re-observes — so if the check swallowed the throw and treated the file as
|
|
1294
|
+
* absent, a later successful read of that path changes the value ⇒ unverified
|
|
1295
|
+
* (spec §3.1, over-record: a throwing access is still an observation).
|
|
1296
|
+
*/
|
|
1297
|
+
recordReadAbsent(repoRelPosixPath) {
|
|
1298
|
+
this._record(observationKey("read", repoRelPosixPath), MISSING_OBSERVATION);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Record an ABSENT directory-listing observation: the check attempted a list
|
|
1302
|
+
* that threw (path allow-checked but the dir was missing/unreadable at list
|
|
1303
|
+
* time). Folds MISSING_OBSERVATION under the list:<path> key — a later
|
|
1304
|
+
* successful listing changes the value ⇒ unverified (spec §3.1, over-record).
|
|
1305
|
+
*/
|
|
1306
|
+
recordListAbsent(repoRelPosixDir) {
|
|
1307
|
+
this._record(observationKey("list", repoRelPosixDir), MISSING_OBSERVATION);
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Record a NEGATIVE graph-node observation: the check looked up a node that
|
|
1311
|
+
* does not exist. Folds the MISSING_OBSERVATION token so the verifier's
|
|
1312
|
+
* re-observation (which reads that node's yg-node.yaml and also yields
|
|
1313
|
+
* MISSING_OBSERVATION when absent) reproduces it byte-for-byte — and creating
|
|
1314
|
+
* the node later changes the value ⇒ unverified (spec §3.1, over-record).
|
|
1315
|
+
*/
|
|
1316
|
+
recordGraphNodeAbsent(nodePath) {
|
|
1317
|
+
this._record(observationKey("graph", nodePath), MISSING_OBSERVATION);
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Record a child-set observation for `nodePath`: the SET of node ids returned
|
|
1321
|
+
* by ctx.graph.children(node). Folds membership only — adding/removing a child
|
|
1322
|
+
* invalidates; a content edit to an unchanged child rides its own graph:
|
|
1323
|
+
* observation (spec §3.1).
|
|
1324
|
+
*/
|
|
1325
|
+
recordGraphChildren(nodePath, childIds) {
|
|
1326
|
+
this._record(observationKey("graph-children", nodePath), hashNodeSetObservation(childIds));
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Record a by-type-set observation for `type`: the SET of node ids returned by
|
|
1330
|
+
* ctx.graph.nodesByType(type). Folds membership only — adding/removing a node
|
|
1331
|
+
* of that type invalidates (spec §3.1).
|
|
1332
|
+
*/
|
|
1333
|
+
recordGraphNodesByType(type, nodeIds) {
|
|
1334
|
+
this._record(observationKey("graph-bytype", type), hashNodeSetObservation(nodeIds));
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Record a flow-participant-set observation for `flowName`: the SET of declared
|
|
1338
|
+
* participant ids of the flow. Folds the flow's participant list (the flow
|
|
1339
|
+
* DEFINITION's membership) so adding/removing a participant in the flow file
|
|
1340
|
+
* invalidates the verdict, even when every still-present participant node is
|
|
1341
|
+
* unchanged (spec §3.1, flowParticipants minor).
|
|
1342
|
+
*/
|
|
1343
|
+
recordFlowParticipants(flowName, participantIds) {
|
|
1344
|
+
this._record(observationKey("graph-flow", flowName), hashNodeSetObservation(participantIds));
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Returns a sorted, deduplicated array of [observationKey, observationHash] pairs.
|
|
1348
|
+
* Re-observing the same key with a different hash sets `tainted = true` and keeps
|
|
1349
|
+
* the first hash (first-observation-wins).
|
|
1350
|
+
*/
|
|
1351
|
+
snapshot() {
|
|
1352
|
+
const result = [...this._entries.entries()];
|
|
1353
|
+
result.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1354
|
+
return result;
|
|
1355
|
+
}
|
|
1356
|
+
/** True if the same path was observed with different content during this run. */
|
|
1357
|
+
get tainted() {
|
|
1358
|
+
return this._tainted;
|
|
1359
|
+
}
|
|
1360
|
+
_record(key, hash) {
|
|
1361
|
+
const existing = this._entries.get(key);
|
|
1362
|
+
if (existing === void 0) {
|
|
1363
|
+
this._entries.set(key, hash);
|
|
1364
|
+
} else if (existing !== hash) {
|
|
1365
|
+
this._tainted = true;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
// src/structure/hook-loader.ts
|
|
1371
|
+
var StructureRunnerError = class extends Error {
|
|
1372
|
+
constructor(code, data) {
|
|
1373
|
+
super(`${code}: ${data.what}
|
|
1374
|
+
${data.why}
|
|
1375
|
+
${data.next}`);
|
|
1376
|
+
this.code = code;
|
|
1377
|
+
this.messageData = data;
|
|
1378
|
+
this.name = "StructureRunnerError";
|
|
1379
|
+
}
|
|
1380
|
+
code;
|
|
1381
|
+
messageData;
|
|
1382
|
+
};
|
|
1383
|
+
async function buildOwnFiles(node, projectRoot, touchedFiles) {
|
|
1384
|
+
const childMappingEntries = [];
|
|
1385
|
+
for (const child of node.children) {
|
|
1386
|
+
for (const raw of child.meta.mapping ?? []) {
|
|
1387
|
+
const p = normalizeMappingPath(raw);
|
|
1388
|
+
if (p) childMappingEntries.push(p);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
const rawMapping = (node.meta.mapping ?? []).map(normalizeMappingPath).filter((p) => p !== "");
|
|
1392
|
+
const expanded = await expandMappingPaths(projectRoot, rawMapping);
|
|
1393
|
+
const result = [];
|
|
1394
|
+
for (const p of expanded) {
|
|
1395
|
+
if (childMappingEntries.length > 0 && isPathInMapping(p, childMappingEntries)) continue;
|
|
1396
|
+
if (BINARY_EXTENSIONS.has(path8.extname(p).toLowerCase())) continue;
|
|
1397
|
+
const abs = path8.resolve(projectRoot, p);
|
|
1398
|
+
let bytes;
|
|
1399
|
+
try {
|
|
1400
|
+
bytes = fs4.readFileSync(abs);
|
|
1401
|
+
} catch {
|
|
1402
|
+
continue;
|
|
1403
|
+
}
|
|
1404
|
+
const content = bytes.toString("utf8");
|
|
1405
|
+
result.push({ file: { path: p, content }, bytes });
|
|
1406
|
+
touchedFiles.push(p);
|
|
1407
|
+
}
|
|
1408
|
+
return result;
|
|
1409
|
+
}
|
|
1410
|
+
function wrapNonSubjectFile(f, repoRelPosixPath, bytes, recorder) {
|
|
1411
|
+
if (bytes === void 0) return f;
|
|
1412
|
+
const { content, ...rest } = f;
|
|
1413
|
+
let recorded = false;
|
|
1414
|
+
const wrapped = { ...rest };
|
|
1415
|
+
Object.defineProperty(wrapped, "content", {
|
|
1416
|
+
enumerable: true,
|
|
1417
|
+
configurable: true,
|
|
1418
|
+
get() {
|
|
1419
|
+
if (!recorded) {
|
|
1420
|
+
recorder.recordRead(repoRelPosixPath, bytes);
|
|
1421
|
+
recorded = true;
|
|
1422
|
+
}
|
|
1423
|
+
return content;
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
return wrapped;
|
|
1427
|
+
}
|
|
1428
|
+
async function enumerateMappedFilesAsync(mappingPaths, projectRoot) {
|
|
1429
|
+
const normalized = mappingPaths.map(normalizeMappingPath).filter((p) => p !== "");
|
|
1430
|
+
return expandMappingPaths(projectRoot, normalized);
|
|
1431
|
+
}
|
|
1432
|
+
async function loadHookModule(params) {
|
|
1433
|
+
ensureLoaderRegistered();
|
|
1434
|
+
const { aspectDir, projectRoot, filename } = params;
|
|
1435
|
+
const resolveFailedCode = params.resolveFailedCode ?? "STRUCTURE_LOADER_RESOLVE_FAILED";
|
|
1436
|
+
const aspectDirAbs = path8.isAbsolute(aspectDir) ? aspectDir : path8.resolve(projectRoot, aspectDir);
|
|
1437
|
+
const modulePath = path8.join(aspectDirAbs, filename);
|
|
1438
|
+
try {
|
|
1439
|
+
return await import(pathToFileURL2(modulePath).href);
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
throw new StructureRunnerError(resolveFailedCode, {
|
|
1442
|
+
what: `Failed to load ${filename} at ${modulePath}: ${err.message}`,
|
|
1443
|
+
why: `The runner dynamically imports the aspect's ${filename} before invoking it.`,
|
|
1444
|
+
next: `Ensure ${filename} exists at the aspect directory and has no unresolved imports.`
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async function buildUnitCtx(params) {
|
|
1449
|
+
const { aspectId, nodePath, graph, projectRoot, astCache, touchedFiles, subjectScope } = params;
|
|
1450
|
+
const node = graph.nodes.get(nodePath);
|
|
1451
|
+
if (!node) {
|
|
1452
|
+
throw new StructureRunnerError("STRUCTURE_NODE_MISSING", {
|
|
1453
|
+
what: `Node '${nodePath}' not in graph.`,
|
|
1454
|
+
why: `The runner resolves the node by path to load its mapped files and aspects.`,
|
|
1455
|
+
next: `Pass an existing node path, or add the node to the graph.`
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
void aspectId;
|
|
1459
|
+
const allowedSet = collectAllowedReadsForAspect(nodePath, graph);
|
|
1460
|
+
const recorder = new ObservationRecorder();
|
|
1461
|
+
const ownFilesRaw = (node.meta.mapping ?? []).map(normalizeMappingPath).filter((p) => p !== "");
|
|
1462
|
+
const ownFilesExpanded = await expandMappingPaths(projectRoot, ownFilesRaw);
|
|
1463
|
+
const subjectFiles = subjectScope !== void 0 ? new Set(subjectScope.map(normalizeMappingPath)) : new Set(ownFilesExpanded);
|
|
1464
|
+
const ctxFs = createCtxFs({ allowedSet, projectRoot, touchedFiles, recorder, subjectFiles });
|
|
1465
|
+
const expandedFilesByNode = /* @__PURE__ */ new Map();
|
|
1466
|
+
for (const id of computeAllowedNodePaths(nodePath, graph)) {
|
|
1467
|
+
const m = graph.nodes.get(id);
|
|
1468
|
+
if (m) expandedFilesByNode.set(id, await enumerateMappedFilesAsync(m.meta.mapping ?? [], projectRoot));
|
|
1469
|
+
}
|
|
1470
|
+
const ctxGraph = createCtxGraph({ currentNodePath: nodePath, graph, projectRoot, touchedFiles, expandedFilesByNode, recorder, subjectFiles });
|
|
1471
|
+
const parsers = createCtxParsers({ allowedSet, projectRoot, touchedFiles, astCache, recorder, subjectFiles });
|
|
1472
|
+
const ownFilesWithBytes = await buildOwnFiles(node, projectRoot, touchedFiles);
|
|
1473
|
+
const ownFiles = ownFilesWithBytes.map((x) => x.file);
|
|
1474
|
+
const bytesByPath = /* @__PURE__ */ new Map();
|
|
1475
|
+
for (const x of ownFilesWithBytes) bytesByPath.set(normalizeMappingPath(x.file.path), x.bytes);
|
|
1476
|
+
await prewarmupAstCache({ astCache, projectRoot, files: ownFiles });
|
|
1477
|
+
const ownFilesEnriched = enrichFilesWithAst(ownFiles, astCache);
|
|
1478
|
+
let nodeFilesEnriched;
|
|
1479
|
+
let ctxFilesEnriched;
|
|
1480
|
+
if (subjectScope !== void 0) {
|
|
1481
|
+
nodeFilesEnriched = recorder !== void 0 ? ownFilesEnriched.map((f) => {
|
|
1482
|
+
const p = normalizeMappingPath(f.path);
|
|
1483
|
+
if (subjectFiles.has(p)) return f;
|
|
1484
|
+
return wrapNonSubjectFile(f, p, bytesByPath.get(p), recorder);
|
|
1485
|
+
}) : ownFilesEnriched;
|
|
1486
|
+
ctxFilesEnriched = ownFilesEnriched.filter((f) => subjectFiles.has(normalizeMappingPath(f.path)));
|
|
1487
|
+
} else {
|
|
1488
|
+
nodeFilesEnriched = ownFilesEnriched;
|
|
1489
|
+
ctxFilesEnriched = ownFilesEnriched;
|
|
1490
|
+
}
|
|
1491
|
+
const ctx = {
|
|
1492
|
+
node: {
|
|
1493
|
+
id: node.path,
|
|
1494
|
+
type: node.meta.type,
|
|
1495
|
+
mapping: node.meta.mapping ?? [],
|
|
1496
|
+
files: nodeFilesEnriched,
|
|
1497
|
+
ports: node.meta.ports ?? {}
|
|
1498
|
+
},
|
|
1499
|
+
files: ctxFilesEnriched,
|
|
1500
|
+
// ctx.subject is the unit's subject file(s): for the deterministic whole-node
|
|
1501
|
+
// case it is the SAME array reference as ctx.files; for a per:file unit it is
|
|
1502
|
+
// exactly the narrowed subject view (also ctx.files here). Identical reference
|
|
1503
|
+
// in both branches keeps the alias contract.
|
|
1504
|
+
subject: ctxFilesEnriched,
|
|
1505
|
+
fs: ctxFs,
|
|
1506
|
+
graph: ctxGraph,
|
|
1507
|
+
parseAst: parsers.parseAst,
|
|
1508
|
+
parseYaml: parsers.parseYaml,
|
|
1509
|
+
parseJson: parsers.parseJson,
|
|
1510
|
+
parseToml: parsers.parseToml
|
|
1511
|
+
};
|
|
1512
|
+
const astInputSet = [...ownFiles];
|
|
1513
|
+
for (const rel of node.meta.relations ?? []) {
|
|
1514
|
+
const target = graph.nodes.get(rel.target);
|
|
1515
|
+
if (!target) continue;
|
|
1516
|
+
for (const p of await enumerateMappedFilesAsync(target.meta.mapping ?? [], projectRoot)) {
|
|
1517
|
+
const abs = path8.resolve(projectRoot, p);
|
|
1518
|
+
try {
|
|
1519
|
+
const content = fs4.readFileSync(abs, "utf8");
|
|
1520
|
+
astInputSet.push({ path: p, content });
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
await prewarmupAstCache({ astCache, projectRoot, files: astInputSet });
|
|
1526
|
+
return { ctx, recorder, node, subjectFiles, ownFiles, astInputSet };
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
// src/structure/runner.ts
|
|
1530
|
+
var SUPPRESS_MARKER_MALFORMED_CODE = "STRUCTURE_SUPPRESS_MARKER_MALFORMED";
|
|
1531
|
+
async function runStructureAspect(params) {
|
|
1532
|
+
const { aspectDir, aspectId, nodePath, graph, projectRoot, subjectScope } = params;
|
|
1533
|
+
const ownCache = !params.parseCache;
|
|
1534
|
+
const astCache = params.parseCache ?? /* @__PURE__ */ new Map();
|
|
1535
|
+
const touchedFiles = [];
|
|
1536
|
+
try {
|
|
1537
|
+
let rangesFor2 = function(filePath) {
|
|
1538
|
+
const existing = rangesByFile.get(filePath);
|
|
1539
|
+
if (existing !== void 0) return existing;
|
|
1540
|
+
const cached = astCache.get(filePath);
|
|
1541
|
+
let ranges;
|
|
1542
|
+
try {
|
|
1543
|
+
if (cached) {
|
|
1544
|
+
ranges = collectSuppressions(cached.ast, filePath, cached.content.split("\n").length, cached.content);
|
|
1545
|
+
} else {
|
|
1546
|
+
const content = contentByPath.get(filePath);
|
|
1547
|
+
ranges = content !== void 0 ? collectSuppressions(void 0, filePath, content.split("\n").length, content) : null;
|
|
1548
|
+
}
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
if (err instanceof SuppressMarkerError) {
|
|
1551
|
+
throw new StructureRunnerError(SUPPRESS_MARKER_MALFORMED_CODE, err.messageData);
|
|
1552
|
+
}
|
|
1553
|
+
throw err;
|
|
1554
|
+
}
|
|
1555
|
+
rangesByFile.set(filePath, ranges);
|
|
1556
|
+
return ranges;
|
|
1557
|
+
};
|
|
1558
|
+
var rangesFor = rangesFor2;
|
|
1559
|
+
const mod = await loadHookModule({ aspectDir, projectRoot, filename: "check.mjs" });
|
|
1560
|
+
const exportCheck = validateCheckModuleExport(mod, {
|
|
1561
|
+
codePrefix: "STRUCTURE",
|
|
1562
|
+
runnerLabel: `aspect '${aspectId}'`
|
|
1563
|
+
});
|
|
1564
|
+
if (!exportCheck.ok) {
|
|
1565
|
+
throw new StructureRunnerError(exportCheck.code, exportCheck.message);
|
|
1566
|
+
}
|
|
1567
|
+
const checkFn = mod.check;
|
|
1568
|
+
const { ctx, recorder, ownFiles, astInputSet } = await buildUnitCtx({
|
|
1569
|
+
aspectId,
|
|
1570
|
+
nodePath,
|
|
1571
|
+
graph,
|
|
1572
|
+
projectRoot,
|
|
1573
|
+
astCache,
|
|
1574
|
+
touchedFiles,
|
|
1575
|
+
subjectScope
|
|
1576
|
+
});
|
|
1577
|
+
let raw;
|
|
1578
|
+
try {
|
|
1579
|
+
raw = checkFn(ctx);
|
|
1580
|
+
} catch (err) {
|
|
1581
|
+
if (err instanceof UndeclaredFsReadError) {
|
|
1582
|
+
return {
|
|
1583
|
+
violations: [{
|
|
1584
|
+
message: `Aspect tried to read undeclared path '${err.path}'. Add a relation in yg-node.yaml to the node owning this path.`,
|
|
1585
|
+
kind: "structure-aspect-undeclared-fs-read",
|
|
1586
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1587
|
+
}],
|
|
1588
|
+
touchedFiles: [],
|
|
1589
|
+
succeeded: false,
|
|
1590
|
+
observations: recorder.snapshot(),
|
|
1591
|
+
observationsTainted: recorder.tainted
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
if (err instanceof UndeclaredGraphReadError) {
|
|
1595
|
+
return {
|
|
1596
|
+
violations: [{
|
|
1597
|
+
message: `Aspect tried to read undeclared graph node '${err.nodePath}'. Add a relation in yg-node.yaml.`,
|
|
1598
|
+
kind: "structure-aspect-undeclared-graph-read",
|
|
1599
|
+
file: `.yggdrasil/aspects/${aspectId}/check.mjs`
|
|
1600
|
+
}],
|
|
1601
|
+
touchedFiles: [],
|
|
1602
|
+
succeeded: false,
|
|
1603
|
+
observations: recorder.snapshot(),
|
|
1604
|
+
observationsTainted: recorder.tainted
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
if (err instanceof ParseAstNotPrewarmedError) {
|
|
1608
|
+
return {
|
|
1609
|
+
violations: [{
|
|
1610
|
+
message: `Aspect called ctx.parseAst on '${err.filePath}', which was not pre-warmed by the dispatcher. Add a declared relation to the node owning this file, or use ctx.parseYaml/Json/Toml if AST is not required.`,
|
|
1611
|
+
kind: "structure-aspect-parseast-not-prewarmed",
|
|
1612
|
+
file: `.yggdrasil/model/${nodePath}/yg-node.yaml`
|
|
1613
|
+
}],
|
|
1614
|
+
touchedFiles: [],
|
|
1615
|
+
succeeded: false,
|
|
1616
|
+
observations: recorder.snapshot(),
|
|
1617
|
+
observationsTainted: recorder.tainted
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_THROWN", {
|
|
1621
|
+
what: `check.mjs threw an exception while running (aspect '${aspectId}').`,
|
|
1622
|
+
why: `${err.message}
|
|
1623
|
+
${err.stack ?? ""}`,
|
|
1624
|
+
next: `Fix the bug in check.mjs, then re-run: yg check --approve`
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
if (raw !== null && typeof raw === "object" && typeof raw.then === "function") {
|
|
1628
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_ASYNC", {
|
|
1629
|
+
what: `check.mjs returned a Promise; only synchronous returns are supported.`,
|
|
1630
|
+
why: `The runner does not await check's return value.`,
|
|
1631
|
+
next: `Refactor check to be synchronous.`
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
if (!Array.isArray(raw)) {
|
|
1635
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
1636
|
+
what: `check.mjs returned ${typeof raw}, expected Violation[].`,
|
|
1637
|
+
why: `The runner reports violations from the array returned by check.`,
|
|
1638
|
+
next: `Return [] or Violation[] from check.`
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
const contextFiles = new Set(ownFiles.map((f) => f.path));
|
|
1642
|
+
for (const t of touchedFiles) contextFiles.add(t);
|
|
1643
|
+
const violations = [];
|
|
1644
|
+
for (const v of raw) {
|
|
1645
|
+
if (typeof v !== "object" || v === null || typeof v.message !== "string") {
|
|
1646
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_RETURN_SHAPE", {
|
|
1647
|
+
what: `Violation entry must be an object with a string 'message' field.`,
|
|
1648
|
+
why: `The runner renders each violation from its message and optional file/line.`,
|
|
1649
|
+
next: `Return objects shaped { message: string, file?: string, line?: number } from check.`
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
const vv = v;
|
|
1653
|
+
if (typeof vv.file === "string" && !contextFiles.has(normalizeMappingPath(vv.file))) {
|
|
1654
|
+
throw new StructureRunnerError("STRUCTURE_CHECK_FILE_NOT_IN_CONTEXT", {
|
|
1655
|
+
what: `Violation references file '${vv.file}' not in ctx (own mapping or touched via ctx.fs/ctx.graph).`,
|
|
1656
|
+
why: `Author cannot synthesize violations against files they were not given.`,
|
|
1657
|
+
next: `Return only violations for files in ctx, or declare a relation to the node owning '${vv.file}'.`
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
violations.push(vv);
|
|
1661
|
+
}
|
|
1662
|
+
const contentByPath = /* @__PURE__ */ new Map();
|
|
1663
|
+
for (const f of [...ownFiles, ...astInputSet]) {
|
|
1664
|
+
contentByPath.set(normalizeMappingPath(f.path), f.content);
|
|
1665
|
+
}
|
|
1666
|
+
const rangesByFile = /* @__PURE__ */ new Map();
|
|
1667
|
+
const visible = violations.filter((v) => {
|
|
1668
|
+
if (typeof v.file !== "string" || typeof v.line !== "number") return true;
|
|
1669
|
+
const ranges = rangesFor2(normalizeMappingPath(v.file));
|
|
1670
|
+
if (!ranges) return true;
|
|
1671
|
+
return !isLineSuppressed(ranges, aspectId, v.line);
|
|
1672
|
+
});
|
|
1673
|
+
return {
|
|
1674
|
+
violations: visible,
|
|
1675
|
+
touchedFiles,
|
|
1676
|
+
succeeded: true,
|
|
1677
|
+
observations: recorder.snapshot(),
|
|
1678
|
+
observationsTainted: recorder.tainted
|
|
1679
|
+
};
|
|
1680
|
+
} finally {
|
|
1681
|
+
if (ownCache) destroyParseCache(astCache);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// src/structure/det-worker-core.ts
|
|
1686
|
+
async function runDetTask(req, graph, projectRoot) {
|
|
1687
|
+
try {
|
|
1688
|
+
const result = await runStructureAspect({
|
|
1689
|
+
aspectDir: req.aspectDir,
|
|
1690
|
+
aspectId: req.aspectId,
|
|
1691
|
+
nodePath: req.nodePath,
|
|
1692
|
+
graph,
|
|
1693
|
+
projectRoot,
|
|
1694
|
+
subjectScope: req.subjectScope
|
|
1695
|
+
});
|
|
1696
|
+
return { id: req.id, ok: true, result };
|
|
1697
|
+
} catch (e) {
|
|
1698
|
+
if (e instanceof StructureRunnerError) {
|
|
1699
|
+
return { id: req.id, ok: false, error: { code: e.code, messageData: e.messageData, message: e.message } };
|
|
1700
|
+
}
|
|
1701
|
+
return { id: req.id, ok: false, error: { message: e instanceof Error ? e.message : String(e) } };
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/structure/det-worker.ts
|
|
1706
|
+
if (parentPort) {
|
|
1707
|
+
const { graph, projectRoot } = workerData;
|
|
1708
|
+
const port = parentPort;
|
|
1709
|
+
port.on("message", (req) => {
|
|
1710
|
+
void runDetTask(req, graph, projectRoot).then((reply) => {
|
|
1711
|
+
port.postMessage(reply);
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
//# sourceMappingURL=det-worker.js.map
|