@getripple/core 1.0.4

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,853 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.highestStagedRisk = exports.buildStagedCheckSummary = exports.listGitChangedDiff = exports.listGitChangedFiles = exports.listGitStagedDiff = exports.listGitStagedFiles = exports.isRippleSourceFile = void 0;
27
+ const child_process_1 = require("child_process");
28
+ const fs = __importStar(require("fs"));
29
+ const path = __importStar(require("path"));
30
+ const ts_morph_1 = require("ts-morph");
31
+ const SOURCE_FILE_RE = /\.(ts|tsx|js|jsx|py)$/i;
32
+ const DEFAULT_STAGED_CHECK_TASK = "Review staged change before commit";
33
+ const RISK_RANK = {
34
+ safe: 1,
35
+ caution: 2,
36
+ dangerous: 3,
37
+ };
38
+ const TEST_FILE_RE = /(^|[\\/])(__tests__|tests?|specs?)([\\/])|(\.test|\.spec)\.(ts|tsx|js|jsx|py)$/i;
39
+ function isRippleSourceFile(filePath) {
40
+ return SOURCE_FILE_RE.test(filePath) && !filePath.endsWith(".d.ts");
41
+ }
42
+ exports.isRippleSourceFile = isRippleSourceFile;
43
+ function listGitStagedFiles(workspaceRoot) {
44
+ try {
45
+ const output = (0, child_process_1.execFileSync)("git", ["diff", "--name-only", "--cached", "--diff-filter=ACMR"], {
46
+ cwd: workspaceRoot,
47
+ encoding: "utf8",
48
+ stdio: ["ignore", "pipe", "pipe"],
49
+ });
50
+ return output
51
+ .split(/\r?\n/)
52
+ .map((line) => line.trim())
53
+ .filter(Boolean);
54
+ }
55
+ catch (err) {
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ throw new Error(`Could not read staged files with git diff --cached: ${message}`);
58
+ }
59
+ }
60
+ exports.listGitStagedFiles = listGitStagedFiles;
61
+ function listGitStagedDiff(workspaceRoot) {
62
+ try {
63
+ return (0, child_process_1.execFileSync)("git", ["diff", "--cached", "--unified=0", "--no-ext-diff"], {
64
+ cwd: workspaceRoot,
65
+ encoding: "utf8",
66
+ stdio: ["ignore", "pipe", "pipe"],
67
+ });
68
+ }
69
+ catch (err) {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ throw new Error(`Could not read staged diff with git diff --cached: ${message}`);
72
+ }
73
+ }
74
+ exports.listGitStagedDiff = listGitStagedDiff;
75
+ function listGitChangedFiles(workspaceRoot, baseRef) {
76
+ try {
77
+ const output = (0, child_process_1.execFileSync)("git", ["diff", "--name-only", "--diff-filter=ACMR", baseRef, "--"], {
78
+ cwd: workspaceRoot,
79
+ encoding: "utf8",
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ return output
83
+ .split(/\r?\n/)
84
+ .map((line) => line.trim())
85
+ .filter(Boolean)
86
+ .concat(listGitUntrackedSourceFiles(workspaceRoot))
87
+ .filter(uniqueString);
88
+ }
89
+ catch (err) {
90
+ const message = err instanceof Error ? err.message : String(err);
91
+ throw new Error(`Could not read changed files with git diff ${baseRef}: ${message}`);
92
+ }
93
+ }
94
+ exports.listGitChangedFiles = listGitChangedFiles;
95
+ function listGitUntrackedSourceFiles(workspaceRoot) {
96
+ try {
97
+ const output = (0, child_process_1.execFileSync)("git", ["ls-files", "--others", "--exclude-standard"], {
98
+ cwd: workspaceRoot,
99
+ encoding: "utf8",
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ });
102
+ return output
103
+ .split(/\r?\n/)
104
+ .map((line) => line.trim())
105
+ .filter((line) => line.length > 0 && isRippleSourceFile(line));
106
+ }
107
+ catch {
108
+ return [];
109
+ }
110
+ }
111
+ function uniqueString(value, index, values) {
112
+ return values.indexOf(value) === index;
113
+ }
114
+ function listGitChangedDiff(workspaceRoot, baseRef) {
115
+ try {
116
+ return (0, child_process_1.execFileSync)("git", ["diff", "--unified=0", "--no-ext-diff", baseRef, "--"], {
117
+ cwd: workspaceRoot,
118
+ encoding: "utf8",
119
+ stdio: ["ignore", "pipe", "pipe"],
120
+ });
121
+ }
122
+ catch (err) {
123
+ const message = err instanceof Error ? err.message : String(err);
124
+ throw new Error(`Could not read changed diff with git diff ${baseRef}: ${message}`);
125
+ }
126
+ }
127
+ exports.listGitChangedDiff = listGitChangedDiff;
128
+ function adapterSignalFor(adapterSupport, capability) {
129
+ const profile = adapterSupport.primaryAdapter.capabilityProfile.find((item) => item.capability === capability);
130
+ if (!profile) {
131
+ return null;
132
+ }
133
+ return {
134
+ capability,
135
+ confidence: profile.confidence,
136
+ agentUse: profile.agentUse,
137
+ reason: profile.reason,
138
+ };
139
+ }
140
+ function uniqueAdapterSignals(signals) {
141
+ const byCapability = new Map();
142
+ signals.forEach((signal) => {
143
+ if (!signal) {
144
+ return;
145
+ }
146
+ const existing = byCapability.get(signal.capability);
147
+ if (!existing || signal.confidence > existing.confidence) {
148
+ byCapability.set(signal.capability, signal);
149
+ }
150
+ });
151
+ return Array.from(byCapability.values()).sort((a, b) => a.capability.localeCompare(b.capability));
152
+ }
153
+ function adapterSignalsForChangedSymbol(adapterSupport, symbol) {
154
+ return uniqueAdapterSignals([
155
+ adapterSignalFor(adapterSupport, "symbols"),
156
+ symbol.callers > 0 || symbol.calls > 0
157
+ ? adapterSignalFor(adapterSupport, "call-edges")
158
+ : null,
159
+ ]);
160
+ }
161
+ function adapterSignalsForContractRisk(adapterSupport, symbol, verificationTargets) {
162
+ return uniqueAdapterSignals([
163
+ ...symbol.adapterSignals,
164
+ verificationTargets.some((target) => TEST_FILE_RE.test(target))
165
+ ? adapterSignalFor(adapterSupport, "tests")
166
+ : null,
167
+ ]);
168
+ }
169
+ function adapterSignalsFromPlan(plan) {
170
+ if (!plan) {
171
+ return [];
172
+ }
173
+ return uniqueAdapterSignals([
174
+ ...plan.readFirst.flatMap((file) => file.adapterSignals ?? []),
175
+ ...plan.readIfNeeded.flatMap((file) => file.adapterSignals ?? []),
176
+ ]);
177
+ }
178
+ function adapterSignalLabel(signal) {
179
+ return `${signal.capability} ${signal.agentUse}/${Math.round(signal.confidence * 100)}%`;
180
+ }
181
+ function findAdapterSignal(signals, capability) {
182
+ return signals.find((signal) => signal.capability === capability);
183
+ }
184
+ function uniqueItems(items) {
185
+ const seen = new Set();
186
+ return items.filter((item) => {
187
+ if (seen.has(item)) {
188
+ return false;
189
+ }
190
+ seen.add(item);
191
+ return true;
192
+ });
193
+ }
194
+ function buildStagedCheckAgentActions(files, missingFiles, adapterSupport) {
195
+ const trustedFindings = [];
196
+ const verifyBeforeCommit = [];
197
+ const manualReviewRequired = [];
198
+ files.forEach((file) => {
199
+ const fileSignal = findAdapterSignal(file.adapterSignals, "files");
200
+ const dependencySignal = findAdapterSignal(file.adapterSignals, "dependencies") ??
201
+ findAdapterSignal(file.adapterSignals, "reverse-dependencies");
202
+ if (fileSignal?.agentUse === "trust") {
203
+ trustedFindings.push(`${file.file}: changed source file detection is trusted (${adapterSignalLabel(fileSignal)}).`);
204
+ }
205
+ if (dependencySignal?.agentUse === "trust") {
206
+ trustedFindings.push(`${file.file}: import/read-first neighborhood is trusted (${adapterSignalLabel(dependencySignal)}).`);
207
+ }
208
+ file.changedSymbols.forEach((symbol) => {
209
+ const symbolSignal = findAdapterSignal(symbol.adapterSignals, "symbols");
210
+ const callSignal = findAdapterSignal(symbol.adapterSignals, "call-edges");
211
+ if (symbolSignal?.agentUse === "trust") {
212
+ trustedFindings.push(`${symbol.symbol}: symbol/export detection is trusted (${adapterSignalLabel(symbolSignal)}).`);
213
+ }
214
+ if (callSignal?.agentUse === "verify") {
215
+ verifyBeforeCommit.push(`${symbol.symbol}: verify callers manually because call edges are partial (${adapterSignalLabel(callSignal)}).`);
216
+ }
217
+ });
218
+ file.verificationTargets.forEach((target) => {
219
+ const testSignal = TEST_FILE_RE.test(target)
220
+ ? findAdapterSignal(file.adapterSignals, "tests")
221
+ : undefined;
222
+ if (testSignal?.agentUse === "verify") {
223
+ verifyBeforeCommit.push(`${target}: run or inspect this verification target; test mapping is verify-only (${adapterSignalLabel(testSignal)}).`);
224
+ }
225
+ else {
226
+ verifyBeforeCommit.push(`${target}: verify this target before commit.`);
227
+ }
228
+ });
229
+ file.contractRisks.forEach((risk) => {
230
+ manualReviewRequired.push(`${risk.symbol}: ${risk.risk} contract risk; review public contract and callers before commit.`);
231
+ });
232
+ });
233
+ missingFiles.forEach((file) => {
234
+ manualReviewRequired.push(`${file}: missing from graph; inspect manually before commit.`);
235
+ });
236
+ adapterSupport.primaryAdapter.capabilityProfile
237
+ .filter((capability) => capability.agentUse === "manual")
238
+ .forEach((capability) => {
239
+ manualReviewRequired.push(`${capability.capability}: adapter cannot provide this signal; inspect manually.`);
240
+ });
241
+ return {
242
+ trustedFindings: uniqueItems(trustedFindings).slice(0, 24),
243
+ verifyBeforeCommit: uniqueItems(verifyBeforeCommit).slice(0, 24),
244
+ manualReviewRequired: uniqueItems(manualReviewRequired).slice(0, 24),
245
+ };
246
+ }
247
+ function buildStagedCheckSummary(engine, options) {
248
+ const mode = options.mode ?? "staged";
249
+ const baseRef = mode === "changed" ? options.baseRef ?? "HEAD" : undefined;
250
+ const sourceFiles = options.stagedFiles.filter(isRippleSourceFile);
251
+ const skippedFiles = options.stagedFiles.filter((file) => !isRippleSourceFile(file));
252
+ const stagedDiff = parseStagedDiff(options.stagedDiff ??
253
+ (mode === "changed"
254
+ ? listGitChangedDiff(options.workspaceRoot, baseRef ?? "HEAD")
255
+ : listGitStagedDiff(options.workspaceRoot)));
256
+ const files = [];
257
+ const missingFiles = [];
258
+ const task = options.task ?? DEFAULT_STAGED_CHECK_TASK;
259
+ const tokenBudget = options.tokenBudget ?? 4000;
260
+ const adapterSupport = engine.getAdapterSupport();
261
+ sourceFiles.forEach((filePath) => {
262
+ const focus = engine.getFileFocusSummary(filePath);
263
+ if (!focus) {
264
+ missingFiles.push(filePath);
265
+ return;
266
+ }
267
+ const plan = engine.planContext(task, filePath, tokenBudget);
268
+ const projectPath = focus.projectPath;
269
+ const fileDiff = stagedDiff.get(projectPath) ?? emptyFileDiff(projectPath);
270
+ const changedSymbols = getChangedSymbolsForFile(engine, options.workspaceRoot, projectPath, fileDiff, mode, baseRef).map((symbol) => ({
271
+ ...symbol,
272
+ adapterSignals: adapterSignalsForChangedSymbol(adapterSupport, symbol),
273
+ }));
274
+ const contractRisks = changedSymbols
275
+ .filter((symbol) => symbol.contractRisk !== "none")
276
+ .map((symbol) => {
277
+ const verificationTargets = plan?.verificationTargets.slice(0, 8) ?? [];
278
+ return {
279
+ file: symbol.file,
280
+ symbol: symbol.symbol,
281
+ risk: symbol.contractRisk,
282
+ reason: symbol.reason,
283
+ callers: symbol.callers,
284
+ exported: symbol.exported,
285
+ verificationTargets,
286
+ adapterSignals: adapterSignalsForContractRisk(adapterSupport, symbol, verificationTargets),
287
+ };
288
+ });
289
+ files.push({
290
+ file: projectPath,
291
+ focus: focus.focusPath,
292
+ modificationRisk: focus.modificationRisk,
293
+ importerCount: focus.importedBy.length,
294
+ symbolCount: focus.symbols.length,
295
+ changedLineRanges: fileDiff.changedLineRanges,
296
+ changedSymbols,
297
+ contractRisks,
298
+ readFirst: plan?.readFirst.map((item) => item.file).slice(0, 6) ?? [],
299
+ symbolFocus: plan?.symbolFocus.map((symbol) => symbol.symbol).slice(0, 6) ?? [],
300
+ verificationTargets: plan?.verificationTargets.slice(0, 8) ?? [],
301
+ adapterSignals: uniqueAdapterSignals([
302
+ ...adapterSignalsFromPlan(plan),
303
+ ...changedSymbols.flatMap((symbol) => symbol.adapterSignals),
304
+ ...contractRisks.flatMap((risk) => risk.adapterSignals),
305
+ ]),
306
+ });
307
+ });
308
+ files.sort((a, b) => {
309
+ const riskDelta = RISK_RANK[b.modificationRisk] - RISK_RANK[a.modificationRisk];
310
+ return riskDelta || a.file.localeCompare(b.file);
311
+ });
312
+ const risk = highestStagedRisk(files);
313
+ const changedSymbols = files.flatMap((file) => file.changedSymbols);
314
+ const contractRisks = files.flatMap((file) => file.contractRisks);
315
+ const agentActions = buildStagedCheckAgentActions(files, missingFiles, adapterSupport);
316
+ return {
317
+ workspace: path.resolve(options.workspaceRoot),
318
+ mode,
319
+ baseRef,
320
+ stagedFiles: sourceFiles.length,
321
+ checkedFiles: files.length,
322
+ skippedFiles,
323
+ missingFiles,
324
+ highestRisk: risk,
325
+ requiresAttention: risk === "dangerous" ||
326
+ risk === "caution" ||
327
+ missingFiles.length > 0 ||
328
+ contractRisks.length > 0,
329
+ adapterSupport,
330
+ agentActions,
331
+ changedSymbols,
332
+ contractRisks,
333
+ files,
334
+ };
335
+ }
336
+ exports.buildStagedCheckSummary = buildStagedCheckSummary;
337
+ function highestStagedRisk(files) {
338
+ let result = "none";
339
+ files.forEach((file) => {
340
+ if (result === "none" || RISK_RANK[file.modificationRisk] > RISK_RANK[result]) {
341
+ result = file.modificationRisk;
342
+ }
343
+ });
344
+ return result;
345
+ }
346
+ exports.highestStagedRisk = highestStagedRisk;
347
+ function parseStagedDiff(diff) {
348
+ const result = new Map();
349
+ let current;
350
+ let currentRange;
351
+ let currentNewLine = 0;
352
+ diff.split(/\r?\n/).forEach((line) => {
353
+ if (line.startsWith("diff --git ")) {
354
+ current = undefined;
355
+ currentRange = undefined;
356
+ currentNewLine = 0;
357
+ return;
358
+ }
359
+ if (line === "new file mode" || line.startsWith("new file mode ")) {
360
+ if (current) {
361
+ current.isNewFile = true;
362
+ }
363
+ return;
364
+ }
365
+ if (line === "deleted file mode" || line.startsWith("deleted file mode ")) {
366
+ if (current) {
367
+ current.isDeletedFile = true;
368
+ }
369
+ return;
370
+ }
371
+ if (line.startsWith("+++ ")) {
372
+ const file = parseDiffFilePath(line.slice(4));
373
+ if (!file) {
374
+ current = undefined;
375
+ return;
376
+ }
377
+ current = ensureParsedDiffFile(result, file);
378
+ return;
379
+ }
380
+ if (!current) {
381
+ return;
382
+ }
383
+ const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
384
+ if (hunk) {
385
+ const start = Number(hunk[1]);
386
+ const count = hunk[2] === undefined ? 1 : Number(hunk[2]);
387
+ currentNewLine = start;
388
+ currentRange = {
389
+ start,
390
+ end: count > 0 ? start + count - 1 : start,
391
+ lineCount: count,
392
+ };
393
+ current.changedLineRanges.push(currentRange);
394
+ return;
395
+ }
396
+ if (!currentRange) {
397
+ return;
398
+ }
399
+ if (line.startsWith("+") && !line.startsWith("+++")) {
400
+ current.changedLines.push({
401
+ line: currentNewLine,
402
+ text: line.slice(1),
403
+ kind: "added",
404
+ });
405
+ currentNewLine++;
406
+ return;
407
+ }
408
+ if (line.startsWith("-") && !line.startsWith("---")) {
409
+ current.changedLines.push({
410
+ line: currentRange.start,
411
+ text: line.slice(1),
412
+ kind: "removed",
413
+ });
414
+ return;
415
+ }
416
+ if (line.startsWith(" ")) {
417
+ currentNewLine++;
418
+ }
419
+ });
420
+ return result;
421
+ }
422
+ function parseDiffFilePath(rawPath) {
423
+ if (rawPath === "/dev/null") {
424
+ return null;
425
+ }
426
+ const withoutPrefix = rawPath.startsWith("b/") ? rawPath.slice(2) : rawPath;
427
+ return withoutPrefix.replace(/\\/g, "/");
428
+ }
429
+ function ensureParsedDiffFile(files, file) {
430
+ const existing = files.get(file);
431
+ if (existing) {
432
+ return existing;
433
+ }
434
+ const created = {
435
+ file,
436
+ isNewFile: false,
437
+ isDeletedFile: false,
438
+ changedLineRanges: [],
439
+ changedLines: [],
440
+ };
441
+ files.set(file, created);
442
+ return created;
443
+ }
444
+ function emptyFileDiff(file) {
445
+ return {
446
+ file,
447
+ isNewFile: false,
448
+ isDeletedFile: false,
449
+ changedLineRanges: [],
450
+ changedLines: [],
451
+ };
452
+ }
453
+ function getChangedSymbolsForFile(engine, workspaceRoot, projectPath, diff, mode, baseRef) {
454
+ if (diff.changedLineRanges.length === 0) {
455
+ return [];
456
+ }
457
+ const content = mode === "changed"
458
+ ? readWorkingTreeFileContent(workspaceRoot, projectPath)
459
+ : readStagedFileContent(workspaceRoot, projectPath);
460
+ if (content === null) {
461
+ return [];
462
+ }
463
+ const symbols = parseSymbolRanges(engine, workspaceRoot, projectPath, content);
464
+ const previousContent = mode === "changed" && baseRef
465
+ ? readGitRefFileContent(workspaceRoot, baseRef, projectPath)
466
+ : readHeadFileContent(workspaceRoot, projectPath);
467
+ const previousSymbols = previousContent === null
468
+ ? new Map()
469
+ : symbolsByName(parseSymbolRanges(engine, workspaceRoot, projectPath, previousContent));
470
+ return symbols
471
+ .filter((symbol) => rangesIntersectSymbol(diff.changedLineRanges, symbol))
472
+ .map((symbol) => buildChangedSymbol(symbol, diff, previousSymbols.get(symbol.name)))
473
+ .sort((a, b) => {
474
+ const riskDelta = contractRiskRank(b.contractRisk) - contractRiskRank(a.contractRisk);
475
+ return riskDelta || a.symbol.localeCompare(b.symbol);
476
+ });
477
+ }
478
+ function readWorkingTreeFileContent(workspaceRoot, projectPath) {
479
+ const absolutePath = path.resolve(workspaceRoot, projectPath);
480
+ try {
481
+ return fs.readFileSync(absolutePath, "utf8");
482
+ }
483
+ catch {
484
+ return null;
485
+ }
486
+ }
487
+ function readStagedFileContent(workspaceRoot, projectPath) {
488
+ try {
489
+ return (0, child_process_1.execFileSync)("git", ["show", `:${projectPath}`], {
490
+ cwd: workspaceRoot,
491
+ encoding: "utf8",
492
+ stdio: ["ignore", "pipe", "pipe"],
493
+ });
494
+ }
495
+ catch {
496
+ const absolutePath = path.resolve(workspaceRoot, projectPath);
497
+ try {
498
+ return fs.readFileSync(absolutePath, "utf8");
499
+ }
500
+ catch {
501
+ return null;
502
+ }
503
+ }
504
+ }
505
+ function readGitRefFileContent(workspaceRoot, ref, projectPath) {
506
+ try {
507
+ return (0, child_process_1.execFileSync)("git", ["show", `${ref}:${projectPath}`], {
508
+ cwd: workspaceRoot,
509
+ encoding: "utf8",
510
+ stdio: ["ignore", "pipe", "pipe"],
511
+ });
512
+ }
513
+ catch {
514
+ return null;
515
+ }
516
+ }
517
+ function readHeadFileContent(workspaceRoot, projectPath) {
518
+ return readGitRefFileContent(workspaceRoot, "HEAD", projectPath);
519
+ }
520
+ function parseSymbolRanges(engine, workspaceRoot, projectPath, content) {
521
+ if (projectPath.toLowerCase().endsWith(".py")) {
522
+ return parsePythonSymbolRanges(engine, workspaceRoot, projectPath, content);
523
+ }
524
+ const project = new ts_morph_1.Project({
525
+ compilerOptions: {
526
+ allowJs: true,
527
+ jsx: 4,
528
+ allowSyntheticDefaultImports: true,
529
+ esModuleInterop: true,
530
+ moduleResolution: 2,
531
+ target: 99,
532
+ strict: false,
533
+ },
534
+ skipAddingFilesFromTsConfig: true,
535
+ skipFileDependencyResolution: true,
536
+ });
537
+ const sourceFile = project.createSourceFile(projectPath, content, { overwrite: true });
538
+ const exportedNames = exportedSymbolNames(sourceFile);
539
+ const graphSymbols = graphSymbolsByName(engine, workspaceRoot, projectPath);
540
+ const ranges = [];
541
+ const seen = new Set();
542
+ const addRange = (name, kind, node) => {
543
+ if (!name || name.includes("{") || name.includes("[")) {
544
+ return;
545
+ }
546
+ const key = `${name}:${kind}:${node.getStart()}`;
547
+ if (seen.has(key)) {
548
+ return;
549
+ }
550
+ seen.add(key);
551
+ const graphSymbol = graphSymbols.get(name);
552
+ const nodeRange = lineRangeForNode(node);
553
+ ranges.push({
554
+ symbol: `${projectPath}::${name}`,
555
+ file: projectPath,
556
+ name,
557
+ kind: graphSymbol?.kind ?? kind,
558
+ layer: graphSymbol?.layer,
559
+ exported: exportedNames.has(name),
560
+ callers: graphSymbol?.calledBy.size ?? 0,
561
+ calls: graphSymbol?.calls.size ?? 0,
562
+ startLine: nodeRange.start,
563
+ endLine: nodeRange.end,
564
+ signatureStartLine: nodeRange.signatureStart,
565
+ signatureEndLine: nodeRange.signatureEnd,
566
+ signatureText: signatureTextForNode(node),
567
+ });
568
+ };
569
+ sourceFile.getFunctions().forEach((funcDecl) => {
570
+ addRange(funcDecl.getName(), "function", funcDecl);
571
+ });
572
+ sourceFile.getClasses().forEach((classDecl) => {
573
+ addRange(classDecl.getName(), "class", classDecl);
574
+ classDecl.getMethods().forEach((methodDecl) => {
575
+ addRange(methodDecl.getName(), "method", methodDecl);
576
+ });
577
+ });
578
+ sourceFile.getVariableDeclarations().forEach((varDecl) => {
579
+ const initializer = varDecl.getInitializer();
580
+ const kind = initializer &&
581
+ (initializer.getKind() === ts_morph_1.SyntaxKind.ArrowFunction ||
582
+ initializer.getKind() === ts_morph_1.SyntaxKind.FunctionExpression)
583
+ ? "function"
584
+ : "variable";
585
+ addRange(varDecl.getName(), kind, varDecl);
586
+ });
587
+ return ranges;
588
+ }
589
+ function parsePythonSymbolRanges(engine, workspaceRoot, projectPath, content) {
590
+ const graphSymbols = graphSymbolsByName(engine, workspaceRoot, projectPath);
591
+ const lines = content.split(/\r?\n/);
592
+ const ranges = [];
593
+ const seen = new Set();
594
+ lines.forEach((line, index) => {
595
+ const functionMatch = /^(\s*)(?:async\s+def|def)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/.exec(line);
596
+ const classMatch = /^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(|:)/.exec(line);
597
+ const match = functionMatch ?? classMatch;
598
+ if (!match) {
599
+ return;
600
+ }
601
+ const indent = pythonIndent(match[1]);
602
+ const name = match[2];
603
+ const kind = classMatch
604
+ ? "class"
605
+ : indent > 0
606
+ ? "method"
607
+ : "function";
608
+ const key = `${name}:${kind}:${index}`;
609
+ if (seen.has(key)) {
610
+ return;
611
+ }
612
+ seen.add(key);
613
+ const graphSymbol = graphSymbols.get(name);
614
+ const startLine = index + 1;
615
+ const endLine = pythonBlockEndLine(lines, index, indent);
616
+ ranges.push({
617
+ symbol: `${projectPath}::${name}`,
618
+ file: projectPath,
619
+ name,
620
+ kind: graphSymbol?.kind ?? kind,
621
+ layer: graphSymbol?.layer,
622
+ exported: indent === 0 && !name.startsWith("_"),
623
+ callers: graphSymbol?.calledBy.size ?? 0,
624
+ calls: graphSymbol?.calls.size ?? 0,
625
+ startLine,
626
+ endLine,
627
+ signatureStartLine: startLine,
628
+ signatureEndLine: startLine,
629
+ signatureText: normalizeSignatureText(line.trim()),
630
+ });
631
+ });
632
+ return ranges;
633
+ }
634
+ function pythonBlockEndLine(lines, startIndex, indent) {
635
+ for (let i = startIndex + 1; i < lines.length; i++) {
636
+ const line = lines[i];
637
+ if (!line.trim() || line.trimStart().startsWith("#") || line.trimStart().startsWith("@")) {
638
+ continue;
639
+ }
640
+ const lineIndent = pythonIndent(line.match(/^\s*/)?.[0] ?? "");
641
+ if (lineIndent <= indent) {
642
+ return i;
643
+ }
644
+ }
645
+ return lines.length;
646
+ }
647
+ function pythonIndent(value) {
648
+ return value.replace(/\t/g, " ").length;
649
+ }
650
+ function symbolsByName(symbols) {
651
+ const result = new Map();
652
+ symbols.forEach((symbol) => {
653
+ result.set(symbol.name, symbol);
654
+ });
655
+ return result;
656
+ }
657
+ function exportedSymbolNames(sourceFile) {
658
+ const exported = new Set();
659
+ sourceFile.getExportedDeclarations().forEach((declarations, exportName) => {
660
+ declarations.forEach((decl) => {
661
+ const declaredName = decl.getName?.();
662
+ exported.add(declaredName ?? exportName);
663
+ });
664
+ });
665
+ return exported;
666
+ }
667
+ function graphSymbolsByName(engine, workspaceRoot, projectPath) {
668
+ const symbols = new Map();
669
+ engine.graph.symbols.forEach((symbol) => {
670
+ if (toProjectPath(workspaceRoot, symbol.file) === projectPath) {
671
+ symbols.set(symbol.name, symbol);
672
+ }
673
+ });
674
+ return symbols;
675
+ }
676
+ function lineRangeForNode(node) {
677
+ const sourceFile = node.getSourceFile();
678
+ const start = sourceFile.getLineAndColumnAtPos(node.getStart()).line;
679
+ const end = sourceFile.getLineAndColumnAtPos(node.getEnd()).line;
680
+ const body = bodyNodeFor(node);
681
+ const bodyStart = body
682
+ ? sourceFile.getLineAndColumnAtPos(body.getStart()).line
683
+ : start;
684
+ return {
685
+ start,
686
+ end,
687
+ signatureStart: start,
688
+ signatureEnd: Math.max(start, Math.min(bodyStart, end)),
689
+ };
690
+ }
691
+ function bodyNodeFor(node) {
692
+ if (ts_morph_1.Node.isFunctionDeclaration(node) ||
693
+ ts_morph_1.Node.isMethodDeclaration(node) ||
694
+ ts_morph_1.Node.isFunctionExpression(node) ||
695
+ ts_morph_1.Node.isArrowFunction(node)) {
696
+ return node.getBody();
697
+ }
698
+ if (ts_morph_1.Node.isClassDeclaration(node)) {
699
+ return node;
700
+ }
701
+ if (ts_morph_1.Node.isVariableDeclaration(node)) {
702
+ const initializer = node.getInitializer();
703
+ if (initializer &&
704
+ (ts_morph_1.Node.isArrowFunction(initializer) || ts_morph_1.Node.isFunctionExpression(initializer))) {
705
+ return initializer.getBody();
706
+ }
707
+ }
708
+ return undefined;
709
+ }
710
+ function signatureTextForNode(node) {
711
+ const sourceFile = node.getSourceFile();
712
+ const fullText = sourceFile.getFullText();
713
+ const body = bodyNodeFor(node);
714
+ if (body && !ts_morph_1.Node.isClassDeclaration(node)) {
715
+ const start = node.getStart();
716
+ const bodyStart = body.getStart();
717
+ if (bodyStart > start) {
718
+ return normalizeSignatureText(fullText.slice(start, bodyStart));
719
+ }
720
+ }
721
+ const text = node.getText();
722
+ if (ts_morph_1.Node.isClassDeclaration(node)) {
723
+ const braceIndex = text.indexOf("{");
724
+ if (braceIndex >= 0) {
725
+ return normalizeSignatureText(text.slice(0, braceIndex));
726
+ }
727
+ }
728
+ return normalizeSignatureText(text);
729
+ }
730
+ function normalizeSignatureText(value) {
731
+ return value.replace(/\s+/g, " ").trim();
732
+ }
733
+ function rangesIntersectSymbol(ranges, symbol) {
734
+ return ranges.some((range) => {
735
+ return range.start <= symbol.endLine && range.end >= symbol.startLine;
736
+ });
737
+ }
738
+ function buildChangedSymbol(symbol, diff, previousSymbol) {
739
+ const changedLines = uniqueSortedNumbers(diff.changedLines
740
+ .filter((line) => line.line >= symbol.startLine && line.line <= symbol.endLine)
741
+ .map((line) => line.line));
742
+ const symbolStatus = previousSymbol ? "modified" : "created";
743
+ const signatureTouched = diff.changedLineRanges.some((range) => {
744
+ return range.start <= symbol.signatureEndLine && range.end >= symbol.signatureStartLine;
745
+ });
746
+ const signatureChanged = previousSymbol
747
+ ? previousSymbol.signatureText !== symbol.signatureText
748
+ : false;
749
+ const contractChanged = previousSymbol
750
+ ? signatureChanged ||
751
+ previousSymbol.kind !== symbol.kind ||
752
+ previousSymbol.exported !== symbol.exported
753
+ : symbolStatus === "created" && (symbol.exported || symbol.callers > 0);
754
+ const returnLineChanged = diff.changedLines.some((line) => {
755
+ return (line.line >= symbol.startLine &&
756
+ line.line <= symbol.endLine &&
757
+ /\breturn\b/.test(line.text));
758
+ });
759
+ const contractRisk = contractRiskForSymbol(symbol, symbolStatus, contractChanged, returnLineChanged);
760
+ const changeKind = contractChanged
761
+ ? "signature-or-contract"
762
+ : returnLineChanged
763
+ ? "return-shape-review"
764
+ : "implementation";
765
+ return {
766
+ symbol: symbol.symbol,
767
+ file: symbol.file,
768
+ name: symbol.name,
769
+ kind: symbol.kind,
770
+ layer: symbol.layer,
771
+ exported: symbol.exported,
772
+ callers: symbol.callers,
773
+ calls: symbol.calls,
774
+ symbolStatus,
775
+ changeKind,
776
+ contractRisk,
777
+ signatureTouched,
778
+ signatureChanged,
779
+ contractChanged,
780
+ returnLineChanged,
781
+ changedLines,
782
+ lineRange: {
783
+ start: symbol.startLine,
784
+ end: symbol.endLine,
785
+ },
786
+ reason: changedSymbolReason(symbol, symbolStatus, signatureTouched, signatureChanged, contractChanged, returnLineChanged, contractRisk),
787
+ adapterSignals: [],
788
+ };
789
+ }
790
+ function contractRiskForSymbol(symbol, symbolStatus, contractChanged, returnLineChanged) {
791
+ if (symbolStatus === "created" && (symbol.exported || symbol.callers > 0)) {
792
+ return "review";
793
+ }
794
+ if (contractChanged && symbol.exported && symbol.callers > 0) {
795
+ return "high";
796
+ }
797
+ if (contractChanged && (symbol.exported || symbol.callers > 0)) {
798
+ return "review";
799
+ }
800
+ if (returnLineChanged && symbol.exported) {
801
+ return "review";
802
+ }
803
+ return "none";
804
+ }
805
+ function changedSymbolReason(symbol, symbolStatus, signatureTouched, signatureChanged, contractChanged, returnLineChanged, contractRisk) {
806
+ const signals = [];
807
+ if (symbolStatus === "created") {
808
+ signals.push("new symbol in staged changes");
809
+ }
810
+ if (signatureChanged) {
811
+ signals.push("signature changed compared with HEAD");
812
+ }
813
+ else if (contractChanged) {
814
+ signals.push("public contract changed compared with HEAD");
815
+ }
816
+ else if (signatureTouched) {
817
+ signals.push("declaration/signature lines touched without signature change");
818
+ }
819
+ if (returnLineChanged) {
820
+ signals.push("changed return line");
821
+ }
822
+ if (symbol.exported) {
823
+ signals.push("exported symbol");
824
+ }
825
+ if (symbol.callers > 0) {
826
+ signals.push(`${symbol.callers} caller(s)`);
827
+ }
828
+ if (signals.length === 0) {
829
+ signals.push("implementation lines changed");
830
+ }
831
+ return contractRisk === "none"
832
+ ? signals.join("; ")
833
+ : `${signals.join("; ")}; contract review recommended`;
834
+ }
835
+ function contractRiskRank(risk) {
836
+ if (risk === "high") {
837
+ return 2;
838
+ }
839
+ if (risk === "review") {
840
+ return 1;
841
+ }
842
+ return 0;
843
+ }
844
+ function uniqueSortedNumbers(values) {
845
+ return Array.from(new Set(values)).sort((a, b) => a - b);
846
+ }
847
+ function toProjectPath(workspaceRoot, filePath) {
848
+ const relativePath = path.isAbsolute(filePath)
849
+ ? path.relative(workspaceRoot, filePath)
850
+ : filePath;
851
+ return relativePath.split(path.sep).join("/");
852
+ }
853
+ //# sourceMappingURL=staged-check.js.map