@moda-ai/cli 1.55.92 → 1.55.94

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.
@@ -0,0 +1,867 @@
1
+ import {
2
+ calleeChain,
3
+ clearExtractCache,
4
+ enumerateAgents,
5
+ extractHarnessWithContext,
6
+ isEvalPath,
7
+ isTestPath,
8
+ receiverClasses,
9
+ relPath,
10
+ resolvePromptArguments,
11
+ resolvedSymbol,
12
+ returnExpressions,
13
+ valueDeclaration
14
+ } from "./cli-5c37h7n5.js";
15
+ import"./cli-k0vxtzxb.js";
16
+ import"./cli-97fnztpy.js";
17
+ import {
18
+ detectBoundRuntimeClasses
19
+ } from "./cli-krm4w2v9.js";
20
+ import"./cli-6ga6kmdp.js";
21
+ import"./cli-dxajrq9h.js";
22
+ import"./cli-13zdcv4w.js";
23
+ import"./cli-e0kbye4s.js";
24
+ import"./cli-1nd4339b.js";
25
+ import"./cli-k4tqwah4.js";
26
+ import {
27
+ require_typescript
28
+ } from "./cli-r0pkdy83.js";
29
+ import"./cli-9kfxkxmk.js";
30
+ import"./cli-0semcakc.js";
31
+ import {
32
+ __toESM
33
+ } from "./cli-0v6na3yp.js";
34
+
35
+ // src/change-impact.ts
36
+ var import_typescript2 = __toESM(require_typescript(), 1);
37
+ import { z } from "zod";
38
+ import { execFileSync } from "node:child_process";
39
+ import { lstatSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
40
+ import { tmpdir } from "node:os";
41
+ import { join, resolve } from "node:path";
42
+
43
+ // src/change-impact-closure.ts
44
+ var import_typescript = __toESM(require_typescript(), 1);
45
+ import { relative } from "node:path";
46
+ var owner = (node) => import_typescript.default.isFunctionLike(node) || import_typescript.default.isVariableDeclaration(node) || import_typescript.default.isClassLike(node) || import_typescript.default.isPropertyDeclaration(node) || import_typescript.default.isPropertyAssignment(node) || import_typescript.default.isInterfaceDeclaration(node) || import_typescript.default.isTypeAliasDeclaration(node) || import_typescript.default.isEnumDeclaration(node) || import_typescript.default.isParameter(node) || import_typescript.default.isBindingElement(node);
47
+ function enclosing(node) {
48
+ for (let at = node;at && !import_typescript.default.isSourceFile(at); at = at.parent)
49
+ if (owner(at))
50
+ return at;
51
+ return;
52
+ }
53
+ var named = (node) => node.name;
54
+ function identity(root, node) {
55
+ return `${relative(root, node.getSourceFile().fileName).replaceAll("\\", "/")}:${node.getStart()}`;
56
+ }
57
+ function coordinate(root, node) {
58
+ const source = node.getSourceFile();
59
+ return {
60
+ path: relPath(root, source.fileName),
61
+ line: source.getLineAndCharacterOfPosition(node.getStart()).line + 1,
62
+ endLine: source.getLineAndCharacterOfPosition(node.getEnd()).line + 1
63
+ };
64
+ }
65
+ function contains(root, node, path, line, col = 1, offset) {
66
+ const source = node.getSourceFile();
67
+ if (relPath(root, source.fileName) !== path)
68
+ return false;
69
+ const position = offset ?? source.getPositionOfLineAndCharacter(line - 1, col - 1);
70
+ return node.getStart() <= position && position < node.getEnd();
71
+ }
72
+ function referenceClosure(ctx, extracted, ranges, limits = {}) {
73
+ const root = ctx.rootDir, maxSymbols = limits.maxSymbols ?? 5000, maxReferences = limits.maxReferences ?? 1e5;
74
+ const warnings = [], reached = new Map, nodes = new Map;
75
+ const queue = [];
76
+ const boundaryClasses = new Set;
77
+ const files = ctx.sourceFiles().filter((source) => {
78
+ const path = coordinate(root, source).path;
79
+ return !isTestPath(path) && !isEvalPath(path);
80
+ });
81
+ const available = new Set(files.map((source) => source.fileName));
82
+ const classes = new Map;
83
+ for (const source of files) {
84
+ const visit = (node) => {
85
+ if (import_typescript.default.isClassLike(node) && node.name)
86
+ classes.set(node.name.text, [...classes.get(node.name.text) ?? [], node]);
87
+ import_typescript.default.forEachChild(node, visit);
88
+ };
89
+ visit(source);
90
+ }
91
+ const bindings = new Map;
92
+ for (const bound of detectBoundRuntimeClasses(root)) {
93
+ const targets = bindings.get(bound.binding) ?? new Set;
94
+ targets.add(bound.className);
95
+ bindings.set(bound.binding, targets);
96
+ }
97
+ const rpcClasses = (expression, checker, depth = 0) => {
98
+ if (depth > 4)
99
+ return [];
100
+ const typed = receiverClasses(expression, checker);
101
+ if (typed.length)
102
+ return typed;
103
+ if (!import_typescript.default.isCallExpression(expression))
104
+ return [];
105
+ const callee = expression.expression;
106
+ if (!import_typescript.default.isPropertyAccessExpression(callee))
107
+ return [];
108
+ const namespace = callee.expression;
109
+ if (/^(get|getByName)$/.test(callee.name.text) && import_typescript.default.isPropertyAccessExpression(namespace) && import_typescript.default.isPropertyAccessExpression(namespace.expression) && namespace.expression.name.text === "env" && namespace.expression.expression.kind === import_typescript.default.SyntaxKind.ThisKeyword) {
110
+ const targets = bindings.get(namespace.name.text);
111
+ if (targets?.size === 1) {
112
+ const candidates = classes.get([...targets][0]) ?? [];
113
+ return candidates.length === 1 ? candidates : [];
114
+ }
115
+ }
116
+ const fn = valueDeclaration(resolvedSymbol(callee.name, checker));
117
+ return fn && (import_typescript.default.isMethodDeclaration(fn) || import_typescript.default.isFunctionDeclaration(fn)) ? returnExpressions(fn).flatMap((value) => rpcClasses(value, checker, depth + 1)) : [];
118
+ };
119
+ const add = (node, via) => {
120
+ if (!node || !available.has(node.getSourceFile().fileName))
121
+ return;
122
+ const id = identity(root, node);
123
+ if (reached.has(id))
124
+ return;
125
+ if (reached.size >= maxSymbols) {
126
+ if (!warnings.includes("symbol closure budget reached"))
127
+ warnings.push("symbol closure budget reached");
128
+ return;
129
+ }
130
+ reached.set(id, { ...coordinate(root, node), id, name: named(node)?.getText() ?? import_typescript.default.SyntaxKind[node.kind], ...via ? { via } : {} });
131
+ nodes.set(id, node);
132
+ queue.push(node);
133
+ };
134
+ const seedImportUses = (declaration, source) => {
135
+ const clause = declaration.importClause;
136
+ if (!clause)
137
+ return;
138
+ const bindings2 = [
139
+ clause.name,
140
+ ...clause.namedBindings && import_typescript.default.isNamespaceImport(clause.namedBindings) ? [clause.namedBindings.name] : [],
141
+ ...clause.namedBindings && import_typescript.default.isNamedImports(clause.namedBindings) ? clause.namedBindings.elements.map((element) => element.name) : []
142
+ ].filter((name) => Boolean(name));
143
+ const checker = ctx.checkerFor(source.fileName);
144
+ const symbols = new Set(bindings2.map((name) => checker?.getSymbolAtLocation(name)).filter(Boolean));
145
+ const names = new Set(bindings2.map((name) => name.text));
146
+ const visit = (node) => {
147
+ if (import_typescript.default.isImportDeclaration(node))
148
+ return;
149
+ if (import_typescript.default.isIdentifier(node) && names.has(node.text) && (checker ? symbols.has(checker.getSymbolAtLocation(node)) : true))
150
+ add(enclosing(node));
151
+ import_typescript.default.forEachChild(node, visit);
152
+ };
153
+ visit(source);
154
+ };
155
+ for (const range of ranges) {
156
+ const source = files.find((file) => coordinate(root, file).path === range.path);
157
+ if (!source)
158
+ continue;
159
+ const starts = source.getLineStarts();
160
+ if (range.endLine - range.line > 5000)
161
+ warnings.push(`changed-line budget reached: ${range.path}`);
162
+ for (let line = range.line;line <= Math.min(range.endLine, range.line + 5000, starts.length); line++) {
163
+ let position = starts[line - 1] + (range.startColumn !== undefined && line === range.line ? range.startColumn - 1 : 0);
164
+ while (position < source.text.length && /[ \t]/.test(source.text[position]))
165
+ position++;
166
+ let innermost = source;
167
+ const find = (node) => {
168
+ if (node.getFullStart() <= position && position < node.getEnd()) {
169
+ innermost = node;
170
+ import_typescript.default.forEachChild(node, find);
171
+ }
172
+ };
173
+ find(source);
174
+ let importDeclaration = innermost;
175
+ while (importDeclaration && !import_typescript.default.isImportDeclaration(importDeclaration) && !import_typescript.default.isSourceFile(importDeclaration))
176
+ importDeclaration = importDeclaration.parent;
177
+ if (importDeclaration && import_typescript.default.isImportDeclaration(importDeclaration)) {
178
+ seedImportUses(importDeclaration, source);
179
+ continue;
180
+ }
181
+ let seed = enclosing(innermost);
182
+ if (!seed) {
183
+ const findDeclaration = (node) => {
184
+ if (seed)
185
+ return;
186
+ const startLine = source.getLineAndCharacterOfPosition(node.getStart()).line + 1;
187
+ if (startLine > line || source.getLineAndCharacterOfPosition(node.getEnd()).line + 1 < line)
188
+ return;
189
+ if (owner(node))
190
+ seed = node;
191
+ else
192
+ import_typescript.default.forEachChild(node, findDeclaration);
193
+ };
194
+ import_typescript.default.forEachChild(source, findDeclaration);
195
+ }
196
+ if (range.addedOnly)
197
+ while (seed && !import_typescript.default.isFunctionLike(seed))
198
+ seed = enclosing(seed.parent);
199
+ add(seed);
200
+ }
201
+ }
202
+ const references = new Map;
203
+ for (const source of files) {
204
+ const checker = ctx.checkerFor(source.fileName);
205
+ if (!checker)
206
+ continue;
207
+ const visit = (node) => {
208
+ if (import_typescript.default.isIdentifier(node) || import_typescript.default.isStringLiteralLike(node)) {
209
+ const symbol = resolvedSymbol(node, checker);
210
+ const declarations = [...symbol?.declarations ?? []];
211
+ if (import_typescript.default.isIdentifier(node) && import_typescript.default.isPropertyAccessExpression(node.parent) && node.parent.name === node && import_typescript.default.isCallExpression(node.parent.parent) && node.parent.parent.expression === node.parent) {
212
+ for (const cls of rpcClasses(node.parent.expression, checker)) {
213
+ for (const member of cls.members)
214
+ if (import_typescript.default.isMethodDeclaration(member) && member.name.getText() === node.text)
215
+ declarations.push(member);
216
+ }
217
+ }
218
+ for (const declaration of new Set(declarations)) {
219
+ if (!available.has(declaration.getSourceFile().fileName))
220
+ continue;
221
+ const id = identity(root, declaration);
222
+ const list = references.get(id) ?? [];
223
+ list.push(node);
224
+ references.set(id, list);
225
+ }
226
+ }
227
+ import_typescript.default.forEachChild(node, visit);
228
+ };
229
+ visit(source);
230
+ }
231
+ let visits = 0;
232
+ for (let index = 0;index < queue.length; index++) {
233
+ const node = queue[index], id = identity(root, node);
234
+ if (import_typescript.default.isMethodDeclaration(node)) {
235
+ const cls = enclosing(node.parent);
236
+ if (cls && import_typescript.default.isClassLike(cls) && extracted.callSites.some((site) => contains(root, cls, site.path, site.line, site.col) && !contains(root, node, site.path, site.line, site.col))) {
237
+ boundaryClasses.add(cls);
238
+ add(cls, id);
239
+ }
240
+ }
241
+ if (import_typescript.default.isVariableDeclaration(node) && (import_typescript.default.isArrayBindingPattern(node.name) || import_typescript.default.isObjectBindingPattern(node.name))) {
242
+ const bindings2 = (pattern) => {
243
+ for (const element of pattern.elements)
244
+ if (import_typescript.default.isBindingElement(element)) {
245
+ add(element, id);
246
+ if (import_typescript.default.isArrayBindingPattern(element.name) || import_typescript.default.isObjectBindingPattern(element.name))
247
+ bindings2(element.name);
248
+ }
249
+ };
250
+ bindings2(node.name);
251
+ }
252
+ if ((import_typescript.default.isArrowFunction(node) || import_typescript.default.isFunctionExpression(node)) && owner(node.parent))
253
+ add(node.parent, id);
254
+ for (const reference of boundaryClasses.has(node) ? [] : references.get(id) ?? []) {
255
+ visits++;
256
+ if (visits > maxReferences) {
257
+ warnings.push("reference closure budget reached");
258
+ break;
259
+ }
260
+ if (reference === named(node))
261
+ continue;
262
+ add(enclosing(reference), id);
263
+ }
264
+ if (visits > maxReferences)
265
+ break;
266
+ if (import_typescript.default.isPropertyAssignment(node) || import_typescript.default.isPropertyDeclaration(node) || import_typescript.default.isParameter(node))
267
+ add(enclosing(node.parent), id);
268
+ }
269
+ const affected = (path, line, col, offset) => [...nodes.values()].some((node) => contains(root, node, path, line, col, offset));
270
+ return {
271
+ symbols: [...reached.values()],
272
+ callsites: extracted.callSites.filter((site) => !site.isTest && !site.isEval && affected(site.path, site.line, site.col)).map((site) => site.id),
273
+ toolIds: extracted.toolDefinitions.filter((tool) => affected(tool.definition.path, tool.definition.line, undefined, tool.definition.start) || ranges.some((range) => range.path === tool.definition.path && range.line <= tool.definition.line && range.endLine >= tool.definition.line)).map((tool) => tool.toolId),
274
+ warnings
275
+ };
276
+ }
277
+
278
+ // src/change-impact.ts
279
+ function descendants(tree) {
280
+ return [
281
+ tree,
282
+ ...(tree.children ?? []).flatMap(descendants),
283
+ ...(tree.bindings ?? []).flatMap(descendants),
284
+ ...tree.callee ? descendants(tree.callee) : []
285
+ ];
286
+ }
287
+ var overlaps = (a, b) => a.path === b.path && a.line <= b.endLine && b.line <= a.endLine;
288
+ var normalize = (text) => text.replace(/\s+/g, " ").trim().toLowerCase();
289
+ function changedOldRanges(diff) {
290
+ const ranges = [];
291
+ let path;
292
+ let pending;
293
+ const finish = () => {
294
+ if (!pending || pending.range.addedOnly || pending.range.line !== pending.range.endLine || pending.old.length !== 1 || pending.next.length !== 1)
295
+ return;
296
+ const before = pending.old[0], after = pending.next[0];
297
+ let prefix = 0, suffix = 0;
298
+ while (prefix < Math.min(before.length, after.length) && before[prefix] === after[prefix])
299
+ prefix++;
300
+ while (suffix < Math.min(before.length, after.length) - prefix && before[before.length - suffix - 1] === after[after.length - suffix - 1])
301
+ suffix++;
302
+ pending.range.startColumn = prefix + 1;
303
+ pending.range.endColumn = before.length - suffix + 1;
304
+ };
305
+ for (const line of diff.split(`
306
+ `)) {
307
+ if (line.startsWith("diff --git ")) {
308
+ finish();
309
+ pending = undefined;
310
+ path = undefined;
311
+ }
312
+ if (line.startsWith("--- a/"))
313
+ path = line.slice(6);
314
+ if (line.startsWith('--- "'))
315
+ throw new Error("Quoted diff paths are unsupported; use paths without tabs or newlines.");
316
+ const hunk = /^@@ -(\d+)(?:,(\d+))? \+\d+(?:,\d+)? @@/.exec(line);
317
+ if (hunk) {
318
+ finish();
319
+ pending = undefined;
320
+ if (!path)
321
+ continue;
322
+ const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
323
+ const start = Math.max(1, Number(hunk[1]));
324
+ const range = { path, line: start, endLine: count ? start + count - 1 : start, ...count ? {} : { addedOnly: true } };
325
+ ranges.push(range);
326
+ pending = { range, old: [], next: [] };
327
+ } else if (pending && line.startsWith("-") && !line.startsWith("--- "))
328
+ pending.old.push(line.slice(1));
329
+ else if (pending && line.startsWith("+") && !line.startsWith("+++ "))
330
+ pending.next.push(line.slice(1));
331
+ }
332
+ finish();
333
+ return ranges;
334
+ }
335
+ function changedNode(node, range, ctx) {
336
+ if (range.addedOnly || !overlaps(node, range))
337
+ return false;
338
+ if (range.startColumn === undefined || range.endColumn === undefined || !ctx)
339
+ return true;
340
+ const source = ctx.nodeAt({ path: node.path, start: node.start, end: node.end })?.getSourceFile();
341
+ if (!source)
342
+ return true;
343
+ const start = source.getPositionOfLineAndCharacter(range.line - 1, range.startColumn - 1);
344
+ const end = source.getPositionOfLineAndCharacter(range.endLine - 1, range.endColumn - 1);
345
+ return start === end ? node.start <= start && node.end >= end : node.start < end && node.end > start;
346
+ }
347
+ var LOG_CALL = /^(?:console|logger|log|Sentry)$|^(?:log|warn|error|info|debug|trace|captureException|captureMessage)$/;
348
+ var MODEL_KEY = /^(?:model|modelId|model_id|provider)$/;
349
+ function nonPromptLiteral(node) {
350
+ let child = node;
351
+ for (let parent = node.parent;parent; child = parent, parent = parent.parent) {
352
+ if (import_typescript2.default.isParenthesizedExpression(parent) || import_typescript2.default.isAsExpression(parent) || import_typescript2.default.isTemplateSpan(parent) || import_typescript2.default.isTemplateExpression(parent) || import_typescript2.default.isBinaryExpression(parent) && parent.operatorToken.kind === import_typescript2.default.SyntaxKind.PlusToken)
353
+ continue;
354
+ if (import_typescript2.default.isThrowStatement(parent) || import_typescript2.default.isCaseClause(parent) || import_typescript2.default.isElementAccessExpression(parent))
355
+ return true;
356
+ if (import_typescript2.default.isBinaryExpression(parent))
357
+ return [
358
+ import_typescript2.default.SyntaxKind.EqualsEqualsEqualsToken,
359
+ import_typescript2.default.SyntaxKind.ExclamationEqualsEqualsToken,
360
+ import_typescript2.default.SyntaxKind.EqualsEqualsToken,
361
+ import_typescript2.default.SyntaxKind.ExclamationEqualsToken
362
+ ].includes(parent.operatorToken.kind);
363
+ if (import_typescript2.default.isNewExpression(parent))
364
+ return /Error$/.test(parent.expression.getText());
365
+ if (import_typescript2.default.isCallExpression(parent) && parent.expression !== child) {
366
+ const chain = calleeChain(parent.expression) ?? [];
367
+ return chain.some((part) => LOG_CALL.test(part)) && (LOG_CALL.test(chain[0] ?? "") || LOG_CALL.test(chain[chain.length - 1] ?? ""));
368
+ }
369
+ if (import_typescript2.default.isPropertyAssignment(parent))
370
+ return MODEL_KEY.test(parent.name.getText());
371
+ return false;
372
+ }
373
+ return false;
374
+ }
375
+ function sliceChangeImpact(extracted, ranges, affectedCallsites = new Set, agentForScope = () => [], ctx, budget = {}) {
376
+ const units = new Map;
377
+ let textBytes = 0;
378
+ const literalOnlyRanges = new Set;
379
+ const contextPerSite = new Map;
380
+ let contextUnits = 0;
381
+ const store = (unit) => {
382
+ const key = JSON.stringify(unit);
383
+ if (units.has(key))
384
+ return true;
385
+ const bytes = Buffer.byteLength(unit.oldText, "utf8");
386
+ if (unit.via === "symbol-reference") {
387
+ const perSite = contextPerSite.get(unit.callsite) ?? 0;
388
+ if (perSite >= (budget.maxContextPerCallsite ?? 20) || contextUnits >= (budget.maxContextUnits ?? 400)) {
389
+ budget.contextTruncated = true;
390
+ return false;
391
+ }
392
+ contextPerSite.set(unit.callsite, perSite + 1);
393
+ }
394
+ if (units.size >= (budget.maxUnits ?? 2000) || textBytes + bytes > (budget.maxTextBytes ?? 2 * 1024 * 1024)) {
395
+ if (unit.via === "changed-text")
396
+ budget.truncated = true;
397
+ else
398
+ budget.contextTruncated = true;
399
+ return false;
400
+ }
401
+ if (unit.via === "symbol-reference")
402
+ contextUnits++;
403
+ units.set(key, unit);
404
+ textBytes += bytes;
405
+ return true;
406
+ };
407
+ if (ctx)
408
+ for (const path of new Set(ranges.filter((range) => !range.addedOnly).map((range) => range.path))) {
409
+ const source = ctx.sourceFiles().find((source2) => relPath(ctx.rootDir, source2.fileName) === path);
410
+ if (!source)
411
+ continue;
412
+ const visit = (node) => {
413
+ const line = source.getLineAndCharacterOfPosition(node.getStart()).line + 1;
414
+ const endLine = source.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
415
+ const location = { path, line, endLine, start: node.getStart(), end: node.getEnd() };
416
+ if (!ranges.some((range) => overlaps(location, range)))
417
+ return;
418
+ const parts = import_typescript2.default.isTemplateExpression(node) ? [node.head.text, ...node.templateSpans.map((span) => span.literal.text)] : undefined;
419
+ const text = import_typescript2.default.isStringLiteralLike(node) ? node.text : parts?.join("{{slot}}");
420
+ if (import_typescript2.default.isStringLiteralLike(node))
421
+ for (const range of ranges) {
422
+ if (range.path !== path || range.addedOnly || range.line !== range.endLine || range.startColumn === undefined || range.endColumn === undefined)
423
+ continue;
424
+ const start = source.getPositionOfLineAndCharacter(range.line - 1, range.startColumn - 1);
425
+ const end = source.getPositionOfLineAndCharacter(range.line - 1, range.endColumn - 1);
426
+ if (node.getStart() <= start && node.getEnd() >= end)
427
+ literalOnlyRanges.add(range);
428
+ }
429
+ if (text && normalize(text) && ranges.some((range) => changedNode(location, range, ctx)) && !(import_typescript2.default.isPropertyAssignment(node.parent) && node.parent.name === node) && !(import_typescript2.default.isImportDeclaration(node.parent) || import_typescript2.default.isExportDeclaration(node.parent)) && !nonPromptLiteral(node)) {
430
+ const tools = extracted.toolDefinitions.filter((tool) => tool.definition.path === path && tool.definition.start <= node.getStart() && tool.definition.end >= node.getEnd());
431
+ const implementationText = tools.some((tool) => {
432
+ for (let parent = node.parent;parent && parent.getStart() >= tool.definition.start; parent = parent.parent) {
433
+ if (import_typescript2.default.isFunctionLike(parent))
434
+ return true;
435
+ if (parent.getStart() === tool.definition.start)
436
+ break;
437
+ }
438
+ return false;
439
+ });
440
+ for (const site of implementationText ? [] : extracted.callSites.filter((site2) => affectedCallsites.has(site2.id) && !site2.isTest && !site2.isEval)) {
441
+ const agents = site.agentIds ?? agentForScope(site.scope);
442
+ const sameOwnerTools = tools.filter((tool) => (tool.agentIds ?? agentForScope(tool.scope)).some((id) => agents.includes(id)));
443
+ const role = sameOwnerTools.length ? "tools" : site.promptArgs.some((arg) => ["system", "instructions", "messages"].includes(arg.role)) ? "instructions" : undefined;
444
+ if (role)
445
+ store({
446
+ path,
447
+ line,
448
+ endLine,
449
+ callsite: site.id,
450
+ scope: site.scope,
451
+ role,
452
+ oldText: text,
453
+ ...parts ? { literalParts: parts } : {},
454
+ agentIds: agents,
455
+ toolIds: sameOwnerTools.map((tool) => tool.toolId),
456
+ via: "changed-text",
457
+ evidence: "reference-closure"
458
+ });
459
+ }
460
+ }
461
+ if (!import_typescript2.default.isTemplateExpression(node))
462
+ import_typescript2.default.forEachChild(node, visit);
463
+ };
464
+ import_typescript2.default.forEachChild(source, visit);
465
+ }
466
+ if (ctx) {
467
+ const sites = extracted.callSites.filter((site) => affectedCallsites.has(site.id) && !site.isTest && !site.isEval);
468
+ const selected = sites.slice(0, 100);
469
+ if (sites.length > selected.length)
470
+ budget.truncated = true;
471
+ const resolution = resolvePromptArguments(selected, ctx, { keepAll: true });
472
+ const touchedLiterals = (range) => {
473
+ if (range.line !== range.endLine || range.startColumn === undefined || range.endColumn === undefined)
474
+ return;
475
+ const source = ctx.sourceFiles().find((file) => relPath(ctx.rootDir, file.fileName) === range.path);
476
+ if (!source)
477
+ return;
478
+ const start = source.getPositionOfLineAndCharacter(range.line - 1, range.startColumn - 1);
479
+ const end = source.getPositionOfLineAndCharacter(range.line - 1, range.endColumn - 1);
480
+ const texts = [];
481
+ const visit = (node) => {
482
+ if (node.getEnd() < start || node.getStart() > end)
483
+ return;
484
+ const touches = start === end ? node.getStart() <= start && node.getEnd() >= end : node.getStart() < end && node.getEnd() > start;
485
+ if (touches && import_typescript2.default.isStringLiteralLike(node))
486
+ texts.push(node.text);
487
+ else if (touches && import_typescript2.default.isTemplateExpression(node))
488
+ texts.push(...[node.head, ...node.templateSpans.map((span) => span.literal)].filter((part) => part.getStart() < end && part.getEnd() > start).map((part) => part.text));
489
+ else
490
+ import_typescript2.default.forEachChild(node, visit);
491
+ };
492
+ visit(source);
493
+ const found = texts.map(normalize).filter(Boolean);
494
+ return found.length ? found : undefined;
495
+ };
496
+ const refChanged = (ref) => ranges.some((range) => {
497
+ if (range.addedOnly || !overlaps(ref, range))
498
+ return false;
499
+ const literals = touchedLiterals(range);
500
+ return !literals || literals.some((text) => normalize(ref.text).includes(text));
501
+ });
502
+ for (const ref of resolution.refs) {
503
+ if (!refChanged(ref) || !normalize(ref.text))
504
+ continue;
505
+ for (const id of ref.callSiteIds) {
506
+ const site = selected.find((site2) => site2.id === id);
507
+ if (!site)
508
+ continue;
509
+ store({
510
+ path: ref.path,
511
+ line: ref.line,
512
+ endLine: ref.endLine,
513
+ callsite: id,
514
+ scope: site.scope,
515
+ role: ref.role,
516
+ oldText: ref.text,
517
+ ...ref.text.includes("{{") ? { literalParts: ref.text.split(/\{\{[^}]*\}\}/) } : {},
518
+ agentIds: site.agentIds ?? agentForScope(site.scope),
519
+ toolIds: [],
520
+ via: "changed-text"
521
+ });
522
+ }
523
+ }
524
+ }
525
+ for (const directOnly of literalOnlyRanges.size === ranges.length && ranges.length ? [true] : [true, false]) {
526
+ for (const role of extracted.roles) {
527
+ if (role.sink.isTest || role.sink.isEval)
528
+ continue;
529
+ for (const node of descendants(role.tree)) {
530
+ const changed = ranges.some((range) => changedNode(node, range, ctx));
531
+ if (changed !== directOnly || !changed && !affectedCallsites.has(role.sink.id))
532
+ continue;
533
+ const texts = node.kind === "literal" && !node.note ? [node.text ?? ""] : node.kind === "template" && node.parts?.some((part) => normalize(part)) ? [node.parts.join("{{slot}}")] : [];
534
+ for (const oldText of texts) {
535
+ if (!normalize(oldText))
536
+ continue;
537
+ const unit = {
538
+ path: node.path,
539
+ line: node.line,
540
+ endLine: node.endLine,
541
+ callsite: role.sink.id,
542
+ scope: role.sink.scope,
543
+ role: role.role,
544
+ oldText,
545
+ ...node.kind === "template" ? { literalParts: node.parts } : {},
546
+ agentIds: role.sink.agentIds ?? agentForScope(role.sink.scope),
547
+ toolIds: role.role === "tools" ? extracted.toolDefinitions.filter((tool) => tool.definition.path === node.path && tool.definition.start <= node.start && tool.definition.end >= node.end).map((tool) => tool.toolId) : [],
548
+ via: changed ? "changed-text" : "symbol-reference"
549
+ };
550
+ store(unit);
551
+ }
552
+ }
553
+ }
554
+ if (ctx)
555
+ for (const tool of extracted.toolDefinitions) {
556
+ const definition = ctx.nodeAt(tool.definition);
557
+ if (!definition)
558
+ continue;
559
+ const agentIds = tool.agentIds ?? agentForScope(tool.scope);
560
+ const sites = extracted.callSites.filter((site) => affectedCallsites.has(site.id) && (agentIds.length ? (site.agentIds ?? agentForScope(site.scope)).some((id) => agentIds.includes(id)) : site.scope === tool.scope));
561
+ if (!sites.length)
562
+ continue;
563
+ const visit = (node) => {
564
+ if (import_typescript2.default.isFunctionLike(node))
565
+ return;
566
+ if ((import_typescript2.default.isPropertyAssignment(node) || import_typescript2.default.isMethodDeclaration(node)) && /^(execute|handler|run)$/.test(node.name.getText()))
567
+ return;
568
+ const parts = import_typescript2.default.isTemplateExpression(node) ? [node.head.text, ...node.templateSpans.map((span) => span.literal.text)] : undefined;
569
+ const text = import_typescript2.default.isStringLiteralLike(node) ? node.text : parts?.some((part) => normalize(part)) ? parts.join("{{slot}}") : undefined;
570
+ if (text !== undefined && !(import_typescript2.default.isPropertyAssignment(node.parent) && node.parent.name === node)) {
571
+ const source = node.getSourceFile();
572
+ const range = {
573
+ path: tool.definition.path,
574
+ line: source.getLineAndCharacterOfPosition(node.getStart()).line + 1,
575
+ endLine: source.getLineAndCharacterOfPosition(node.getEnd()).line + 1
576
+ };
577
+ for (const site of sites) {
578
+ const unit = {
579
+ ...range,
580
+ callsite: site.id,
581
+ scope: site.scope,
582
+ role: "tools",
583
+ oldText: text,
584
+ ...parts ? { literalParts: parts } : {},
585
+ agentIds: (site.agentIds ?? agentForScope(site.scope)).filter((id) => !agentIds.length || agentIds.includes(id)),
586
+ toolIds: [tool.toolId],
587
+ via: ranges.some((changed) => changedNode({ ...range, start: node.getStart(), end: node.getEnd() }, changed, ctx)) ? "changed-text" : "symbol-reference"
588
+ };
589
+ if (unit.via === "changed-text" === directOnly)
590
+ store(unit);
591
+ }
592
+ }
593
+ import_typescript2.default.forEachChild(node, visit);
594
+ };
595
+ visit(definition);
596
+ }
597
+ }
598
+ return [...units.values()];
599
+ }
600
+ function unitTextMatches(unit, text) {
601
+ const haystack = normalize(text);
602
+ let cursor = 0;
603
+ for (const part of unit.literalParts ?? [unit.oldText]) {
604
+ const needle = normalize(part);
605
+ if (!needle)
606
+ continue;
607
+ const at = haystack.indexOf(needle, cursor);
608
+ if (at < 0)
609
+ return false;
610
+ cursor = at + needle.length;
611
+ }
612
+ return true;
613
+ }
614
+ var replayRole = (role) => ["tools", "system", "developer", "instructions", "prompt"].includes(role);
615
+ function replayCandidates(units, calls) {
616
+ const result = new Map;
617
+ const directSites = new Set(units.filter((unit) => unit.via === "changed-text" && replayRole(unit.role)).map((unit) => unit.callsite));
618
+ for (const call of calls) {
619
+ if (call.candidate.status !== "ready")
620
+ continue;
621
+ const matches = units.filter((unit) => {
622
+ if (directSites.has(unit.callsite) && unit.via !== "changed-text")
623
+ return false;
624
+ if (call.agentIds?.length && unit.agentIds.length && !unit.agentIds.some((id) => call.agentIds.includes(id)))
625
+ return false;
626
+ const sections = unit.role === "tools" ? call.tools : ["system", "developer", "instructions", "prompt"].includes(unit.role) ? call.sections : [];
627
+ return sections.some((section) => section.sources.some((source) => overlaps(source, unit)) && unitTextMatches(unit, section.text));
628
+ });
629
+ if (matches.length)
630
+ result.set(call.candidate.span_id, {
631
+ ...call.candidate,
632
+ reason: `Old source text matched ${matches.length} source-proven instruction/tool unit(s).`
633
+ });
634
+ }
635
+ return [...result.values()];
636
+ }
637
+ function joinRecordedTools(tools, units, definitions, agentIds, catalog = tools) {
638
+ const strings = (value) => {
639
+ if (typeof value === "string")
640
+ return [value];
641
+ if (Array.isArray(value))
642
+ return value.flatMap(strings);
643
+ return value && typeof value === "object" ? Object.values(value).flatMap(strings) : [];
644
+ };
645
+ return definitions.flatMap((recorded) => {
646
+ const matches = catalog.filter((tool2) => tool2.name === recorded.name && (!agentIds?.length || tool2.agentIds.some((id) => agentIds.includes(id))));
647
+ const distinct = [...new Map(matches.map((tool2) => [`${tool2.id}:${tool2.path}:${tool2.line}`, tool2])).values()];
648
+ if (distinct.length !== 1 || !tools.some((tool2) => tool2.id === distinct[0].id))
649
+ return [];
650
+ const tool = distinct[0];
651
+ let definition = recorded.definition;
652
+ if (typeof definition === "string") {
653
+ try {
654
+ definition = JSON.parse(definition);
655
+ } catch {
656
+ return [];
657
+ }
658
+ }
659
+ const text = strings(definition).join(`
660
+ `);
661
+ const sources = units.filter((unit) => unit.role === "tools" && unit.toolIds.includes(tool.id) && unit.path === tool.path && (!agentIds?.length || unit.agentIds.some((id) => agentIds.includes(id))) && normalize(unit.oldText) && unitTextMatches(unit, text)).map((unit) => ({ path: unit.path, line: unit.line, endLine: unit.endLine }));
662
+ return sources.length ? [{ text, sources }] : [];
663
+ });
664
+ }
665
+ var SNAPSHOT_SOURCE = /\.(?:[cm]?[jt]sx?|svelte|jsonc?|md|txt|ya?ml|toml)$/i;
666
+ function diffWithout(diff, paths) {
667
+ const out = [];
668
+ let section = [];
669
+ let drop = false;
670
+ const flush = () => {
671
+ if (!drop)
672
+ out.push(...section);
673
+ section = [];
674
+ drop = false;
675
+ };
676
+ const lines = diff.split(`
677
+ `);
678
+ lines.forEach((line, i) => {
679
+ const starts = line.startsWith("diff --git ") || line.startsWith("--- ") && lines[i + 1]?.startsWith("+++ ") === true && section.some((l) => l.startsWith("--- "));
680
+ if (starts)
681
+ flush();
682
+ if (line.startsWith("--- a/") && paths.has(line.slice(6)))
683
+ drop = true;
684
+ section.push(line);
685
+ });
686
+ flush();
687
+ return out.join(`
688
+ `);
689
+ }
690
+ var NON_PROMPT_ASSET = /\.(?:css|scss|sass|less|png|jpe?g|gif|webp|avif|ico|bmp|tiff?|svg|mp[34]|mov|webm|wav|ogg|woff2?|ttf|otf|eot|pdf|zip|gz|map)$/i;
691
+ function assertNoLinks(dir) {
692
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
693
+ const path = join(dir, entry.name);
694
+ if (entry.isDirectory())
695
+ assertNoLinks(path);
696
+ else if (!entry.isFile() || lstatSync(path).isSymbolicLink())
697
+ throw new Error(`Refusing a non-regular file in the source snapshot: ${entry.name}`);
698
+ }
699
+ }
700
+ function localChangeImpact(options) {
701
+ if (options.to && options.diff !== undefined)
702
+ throw new Error("Supply either --to or --diff, not both.");
703
+ const started = performance.now();
704
+ const checkout = resolve(options.checkout);
705
+ const git = (args) => execFileSync("git", ["-C", checkout, ...args], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
706
+ const from = git(["rev-parse", "--verify", "--end-of-options", `${options.from}^{commit}`]).trim();
707
+ const to = options.to ? git(["rev-parse", "--verify", "--end-of-options", `${options.to}^{commit}`]).trim() : undefined;
708
+ if (!to && options.diff === undefined)
709
+ throw new Error("Supply --to or --diff with an explicit old-side --from revision.");
710
+ const skippedFiles = [];
711
+ const ignoredFiles = [];
712
+ let diff = options.diff;
713
+ const size = (revision, path) => {
714
+ const output = execFileSync("git", ["-C", checkout, "cat-file", "--batch-check=%(objectsize)"], {
715
+ input: `${revision}:${path}
716
+ `,
717
+ encoding: "utf8",
718
+ stdio: ["pipe", "pipe", "pipe"],
719
+ maxBuffer: 1024 * 1024
720
+ }).trim();
721
+ const bytes = Number(output);
722
+ return Number.isFinite(bytes) ? bytes : 0;
723
+ };
724
+ if (diff !== undefined) {
725
+ const paths = [...new Set(changedOldRanges(diff).map((range) => range.path))];
726
+ const dropped = new Set;
727
+ for (const path of paths) {
728
+ if (NON_PROMPT_ASSET.test(path)) {
729
+ ignoredFiles.push(path);
730
+ dropped.add(path);
731
+ } else if (!SNAPSHOT_SOURCE.test(path)) {
732
+ skippedFiles.push({ path, reason: "unsupported source format" });
733
+ dropped.add(path);
734
+ } else if (size(from, path) > 1024 * 1024) {
735
+ skippedFiles.push({ path, reason: "source size budget exceeded" });
736
+ dropped.add(path);
737
+ }
738
+ }
739
+ if (dropped.size)
740
+ diff = diffWithout(diff, dropped);
741
+ } else {
742
+ const paths = git(["diff", "--name-only", "--no-renames", "-z", from, to, "--"]).split("\x00").filter(Boolean);
743
+ const selected = [];
744
+ let bytes = 0;
745
+ for (const path of paths) {
746
+ if (!/[\r\n]/.test(path) && NON_PROMPT_ASSET.test(path)) {
747
+ ignoredFiles.push(path);
748
+ continue;
749
+ }
750
+ if (/[\r\n]/.test(path) || !SNAPSHOT_SOURCE.test(path)) {
751
+ skippedFiles.push({ path, reason: "unsupported source format" });
752
+ continue;
753
+ }
754
+ const oldBytes = size(from, path), newBytes = size(to, path);
755
+ if (Math.max(oldBytes, newBytes) > 1024 * 1024 || bytes + oldBytes + newBytes > 16 * 1024 * 1024) {
756
+ skippedFiles.push({ path, reason: "source size budget exceeded" });
757
+ continue;
758
+ }
759
+ bytes += oldBytes + newBytes;
760
+ selected.push(`:(literal)${path}`);
761
+ }
762
+ diff = selected.length ? git(["diff", "--no-ext-diff", "--no-textconv", "--unified=0", "--no-renames", from, to, "--", ...selected]) : "";
763
+ }
764
+ const ranges = changedOldRanges(diff);
765
+ const temporary = mkdtempSync(join(tmpdir(), "moda-impact-"));
766
+ try {
767
+ const tree = join(temporary, "checkout");
768
+ if (ranges.length) {
769
+ mkdirSync(tree);
770
+ const archive = join(temporary, "source.tar.gz");
771
+ const entries = git(["ls-tree", "-rl", "-z", from]).split("\x00").filter(Boolean);
772
+ const paths = entries.flatMap((entry) => {
773
+ const match = /^100(?:644|755) blob [a-f0-9]+\s+(\d+)\t([\s\S]+)$/.exec(entry);
774
+ return match && Number(match[1]) <= 1024 * 1024 && /\.(?:[cm]?[jt]sx?|svelte|jsonc?|md|txt|ya?ml|toml|prompt|hbs|jinja2?)$/i.test(match[2]) ? [match[2]] : [];
775
+ });
776
+ if (paths.length) {
777
+ execFileSync("git", ["-C", checkout, "archive", "--format=tar.gz", `--output=${archive}`, from, "--", ...paths.map((path) => `:(literal)${path}`)]);
778
+ execFileSync("tar", ["-xzf", archive, "-C", tree]);
779
+ assertNoLinks(tree);
780
+ }
781
+ const archived = new Set(paths);
782
+ const reported = new Set(skippedFiles.map((file) => file.path));
783
+ for (const path of new Set(ranges.map((range) => range.path))) {
784
+ if (!archived.has(path) && !reported.has(path))
785
+ skippedFiles.push({ path, reason: "not a regular file in the old revision" });
786
+ }
787
+ }
788
+ const output = ranges.length ? extractHarnessWithContext(tree, { status: false, fresh: true }) : undefined;
789
+ const extracted = output?.result;
790
+ const closure = output ? referenceClosure(output.ctx, output.result, ranges, options.limits) : { symbols: [], callsites: [], toolIds: [], warnings: [] };
791
+ const owners = output ? enumerateAgents(output.result, output.ctx) : undefined;
792
+ const unitBudget = { ...options.limits };
793
+ const units = extracted ? sliceChangeImpact(extracted, ranges, new Set(closure.callsites), owners?.agentForScope, output?.ctx, unitBudget) : [];
794
+ const agentIds = new Set(units.flatMap((unit) => unit.agentIds));
795
+ const toolIds = new Set([...closure.toolIds, ...units.flatMap((unit) => unit.toolIds)]);
796
+ const catalog = extracted?.toolDefinitions.map((tool) => ({
797
+ id: tool.toolId,
798
+ name: tool.name,
799
+ path: tool.definition.path,
800
+ line: tool.definition.line,
801
+ agentIds: tool.agentIds ?? owners?.agentForScope(tool.scope) ?? [],
802
+ scope: tool.scope
803
+ })) ?? [];
804
+ const tools = catalog.filter((tool) => toolIds.has(tool.id));
805
+ const calls = (options.calls ?? []).filter((call) => call.sourceCommit === from).map((call) => ({
806
+ ...call,
807
+ tools: [...call.tools, ...joinRecordedTools(tools, units, call.toolDefinitions ?? [], call.agentIds, catalog)]
808
+ }));
809
+ return {
810
+ schemaVersion: 1,
811
+ from,
812
+ to: to ?? null,
813
+ durationMs: Math.round(performance.now() - started),
814
+ ranges,
815
+ skippedFiles,
816
+ ignoredFiles,
817
+ units,
818
+ closure,
819
+ unitSliceTruncated: unitBudget.truncated ?? false,
820
+ contextTruncated: unitBudget.contextTruncated ?? false,
821
+ agents: owners?.runtimeAgents.filter((agent) => agentIds.has(agent.id)) ?? [],
822
+ tools,
823
+ candidates: replayCandidates(units, calls),
824
+ warnings: [...extracted?.warnings ?? [], ...closure.warnings, ...unitBudget.truncated ? ["impact unit/text budget reached; result is incomplete"] : [], ...skippedFiles.length ? [`${skippedFiles.length} changed file(s) outside supported source/size budgets`] : []],
825
+ limitations: [
826
+ "TypeScript/JavaScript extractor only; dynamic unresolved values are not attributed. Source snapshots omit assets and files above 1 MiB.",
827
+ "Calls with a sourceCommit different from the resolved old commit are excluded. Additions use old enclosing context, not invented old text. Call records must carry same-agent backend source proofs.",
828
+ "No production precision/recall measurement is implied by this local result."
829
+ ]
830
+ };
831
+ } finally {
832
+ clearExtractCache(join(temporary, "checkout"));
833
+ rmSync(temporary, { recursive: true, force: true });
834
+ }
835
+ }
836
+ var sourceSchema = z.object({ path: z.string().min(1), line: z.number().int().positive(), endLine: z.number().int().positive() }).refine((source) => source.endLine >= source.line, "endLine must be at least line");
837
+ var recordedCallSchema = z.object({
838
+ agentIds: z.array(z.string()).optional(),
839
+ toolDefinitions: z.array(z.object({ name: z.string(), definition: z.unknown() })).optional(),
840
+ sourceCommit: z.string().regex(/^[a-f0-9]{40,64}$/),
841
+ candidate: z.object({
842
+ id: z.string(),
843
+ conversation_id: z.string().min(1),
844
+ span_id: z.string().min(1),
845
+ started_at: z.string(),
846
+ model: z.string(),
847
+ status: z.enum(["ready", "skipped"]),
848
+ reason: z.string(),
849
+ ask: z.string(),
850
+ tail: z.array(z.object({ role: z.string(), text: z.string() })),
851
+ production: z.object({ text: z.string(), toolCalls: z.array(z.object({ name: z.string(), args: z.unknown() })) })
852
+ }).passthrough(),
853
+ sections: z.array(z.object({ text: z.string(), sources: z.array(sourceSchema) })),
854
+ tools: z.array(z.object({ text: z.string(), sources: z.array(sourceSchema) }))
855
+ });
856
+ function readRecordedImpactCalls(path) {
857
+ return z.array(recordedCallSchema).parse(JSON.parse(readFileSync(path, "utf8")));
858
+ }
859
+ export {
860
+ sliceChangeImpact,
861
+ replayCandidates,
862
+ readRecordedImpactCalls,
863
+ nonPromptLiteral,
864
+ localChangeImpact,
865
+ joinRecordedTools,
866
+ changedOldRanges
867
+ };