@neurcode-ai/cli 0.16.5 → 0.16.7

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.
Files changed (56) hide show
  1. package/LICENSE +201 -0
  2. package/dist/commands/brain.d.ts.map +1 -1
  3. package/dist/commands/brain.js +151 -0
  4. package/dist/commands/brain.js.map +1 -1
  5. package/dist/commands/eval.d.ts +19 -0
  6. package/dist/commands/eval.d.ts.map +1 -0
  7. package/dist/commands/eval.js +246 -0
  8. package/dist/commands/eval.js.map +1 -0
  9. package/dist/commands/onboard.d.ts +29 -0
  10. package/dist/commands/onboard.d.ts.map +1 -0
  11. package/dist/commands/onboard.js +247 -0
  12. package/dist/commands/onboard.js.map +1 -0
  13. package/dist/commands/runtime-sync.d.ts.map +1 -1
  14. package/dist/commands/runtime-sync.js +22 -0
  15. package/dist/commands/runtime-sync.js.map +1 -1
  16. package/dist/commands/session-hook.d.ts.map +1 -1
  17. package/dist/commands/session-hook.js +221 -102
  18. package/dist/commands/session-hook.js.map +1 -1
  19. package/dist/commands/session.d.ts.map +1 -1
  20. package/dist/commands/session.js +31 -0
  21. package/dist/commands/session.js.map +1 -1
  22. package/dist/index.js +9 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/runtime-build.json +5 -5
  25. package/dist/utils/guided-eval.d.ts +251 -0
  26. package/dist/utils/guided-eval.d.ts.map +1 -0
  27. package/dist/utils/guided-eval.js +894 -0
  28. package/dist/utils/guided-eval.js.map +1 -0
  29. package/dist/utils/local-repo-brain.d.ts +158 -0
  30. package/dist/utils/local-repo-brain.d.ts.map +1 -0
  31. package/dist/utils/local-repo-brain.js +854 -0
  32. package/dist/utils/local-repo-brain.js.map +1 -0
  33. package/dist/utils/structural-understanding.d.ts +61 -1
  34. package/dist/utils/structural-understanding.d.ts.map +1 -1
  35. package/dist/utils/structural-understanding.js +534 -1
  36. package/dist/utils/structural-understanding.js.map +1 -1
  37. package/package.json +9 -11
  38. package/.telemetry-bundle/dist/__tests__/harvest-verify.test.d.ts +0 -1
  39. package/.telemetry-bundle/dist/__tests__/harvest-verify.test.js +0 -86
  40. package/.telemetry-bundle/dist/contracts.d.ts +0 -58
  41. package/.telemetry-bundle/dist/contracts.js +0 -8
  42. package/.telemetry-bundle/dist/harvest-verify.d.ts +0 -9
  43. package/.telemetry-bundle/dist/harvest-verify.js +0 -128
  44. package/.telemetry-bundle/dist/index.d.ts +0 -10
  45. package/.telemetry-bundle/dist/index.js +0 -22
  46. package/.telemetry-bundle/dist/precision/leaderboards.d.ts +0 -20
  47. package/.telemetry-bundle/dist/precision/leaderboards.js +0 -72
  48. package/.telemetry-bundle/dist/reader.d.ts +0 -5
  49. package/.telemetry-bundle/dist/reader.js +0 -46
  50. package/.telemetry-bundle/dist/stable-json.d.ts +0 -5
  51. package/.telemetry-bundle/dist/stable-json.js +0 -24
  52. package/.telemetry-bundle/dist/store.d.ts +0 -10
  53. package/.telemetry-bundle/dist/store.js +0 -52
  54. package/.telemetry-bundle/dist/trust-scoring.d.ts +0 -20
  55. package/.telemetry-bundle/dist/trust-scoring.js +0 -58
  56. package/.telemetry-bundle/package.json +0 -8
@@ -48,6 +48,10 @@ exports.STRUCTURAL_UNDERSTANDING_SCHEMA_VERSION = 'neurcode.structural-understan
48
48
  exports.CONSEQUENCE_UNDERSTANDING_SCHEMA_VERSION = 'neurcode.consequence-understanding.v1';
49
49
  const TS_LIKE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/i;
50
50
  const SKIP_DIR = /^(node_modules|\.git|dist|build|out|coverage|\.next|vendor)$/i;
51
+ const REUSE_MIN_TOKEN_COUNT = 12;
52
+ const REUSE_TOKEN_SHINGLE_SIZE = 5;
53
+ const REUSE_TOKEN_OVERLAP_THRESHOLD = 0.86;
54
+ const REUSE_MAX_FINDINGS = 20;
51
55
  const EFFECT_REGISTRY = [
52
56
  {
53
57
  category: 'filesystem-write',
@@ -197,6 +201,27 @@ function emptyConsequenceUnderstanding(generatedAt, reason = null) {
197
201
  };
198
202
  return { ...core, artifactHash: hash(consequenceHashInput(core)) };
199
203
  }
204
+ function emptyRepoSymbolIndexSummary() {
205
+ return {
206
+ schemaVersion: 'neurcode.repo-symbol-index.v1',
207
+ language: 'typescript/javascript',
208
+ modelUsed: false,
209
+ sourceUploaded: false,
210
+ sourceStored: false,
211
+ indexedFileCount: 0,
212
+ indexedSymbolCount: 0,
213
+ exportedSymbolCount: 0,
214
+ localFunctionCount: 0,
215
+ methodCount: 0,
216
+ classCount: 0,
217
+ changedCandidateCount: 0,
218
+ unsupportedLanguages: ['python'],
219
+ indexHash: hash('empty-repo-symbol-index'),
220
+ fingerprintAlgorithm: 'typescript-scanner-normalized-token-shingles-v1',
221
+ signatureAlgorithm: 'name-insensitive-kind-arity-param-return-shape-v1',
222
+ provenance: 'repo-symbol-index',
223
+ };
224
+ }
200
225
  function none(projectRoot, diffFiles, reason, generatedAt, suppressedArtifacts = []) {
201
226
  const core = {
202
227
  schemaVersion: exports.STRUCTURAL_UNDERSTANDING_SCHEMA_VERSION,
@@ -222,6 +247,8 @@ function none(projectRoot, diffFiles, reason, generatedAt, suppressedArtifacts =
222
247
  testReferences: [],
223
248
  digest: emptyDigest(generatedAt),
224
249
  consequenceUnderstanding: emptyConsequenceUnderstanding(generatedAt, reason),
250
+ repoSymbolIndex: emptyRepoSymbolIndexSummary(),
251
+ reuseFindings: [],
225
252
  planAlignment: null,
226
253
  boundaryImpact: [],
227
254
  limitations: commonLimitations(),
@@ -236,7 +263,7 @@ function privacyBlock() {
236
263
  diffStored: false,
237
264
  modelUsed: false,
238
265
  factsOnly: true,
239
- outputContains: ['repo-relative paths', 'symbol names', 'line numbers', 'import targets', 'owner tokens', 'hashes'],
266
+ outputContains: ['repo-relative paths', 'symbol names', 'line numbers', 'import targets', 'owner tokens', 'hashes', 'signature hashes', 'token fingerprints'],
240
267
  outputOmits: ['source text', 'diff hunks', 'patch content', 'shell command bodies', 'model judgments'],
241
268
  };
242
269
  }
@@ -260,6 +287,8 @@ function provenanceMap() {
260
287
  consequenceUnderstanding: 'typescript-checker',
261
288
  planAlignment: 'session-plan',
262
289
  boundaryImpact: 'codeowners-profile',
290
+ repoSymbolIndex: 'repo-symbol-index',
291
+ reuseFindings: 'repo-symbol-index',
263
292
  };
264
293
  }
265
294
  function changedFileFacts(diffFiles) {
@@ -926,6 +955,501 @@ function collectDeclarations(sourceFile, checker, projectRoot) {
926
955
  ts.forEachChild(sourceFile, visit);
927
956
  return out;
928
957
  }
958
+ function isFunctionLikeVariableDeclaration(node) {
959
+ return ts.isVariableDeclaration(node) &&
960
+ Boolean(node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)));
961
+ }
962
+ function functionLikeVariableInitializer(node) {
963
+ if (!ts.isVariableDeclaration(node))
964
+ return null;
965
+ const initializer = node.initializer;
966
+ return initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))
967
+ ? initializer
968
+ : null;
969
+ }
970
+ function reusableKind(node) {
971
+ if (ts.isClassDeclaration(node))
972
+ return 'class';
973
+ if (ts.isMethodDeclaration(node))
974
+ return 'method';
975
+ if (ts.isFunctionDeclaration(node) || isFunctionLikeVariableDeclaration(node))
976
+ return 'function';
977
+ return null;
978
+ }
979
+ function enclosingClassDeclaration(node) {
980
+ let current = node.parent;
981
+ while (current) {
982
+ if (ts.isClassDeclaration(current))
983
+ return current;
984
+ current = current.parent;
985
+ }
986
+ return null;
987
+ }
988
+ function isTopLevelReusableDeclaration(node) {
989
+ if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) {
990
+ return ts.isSourceFile(node.parent);
991
+ }
992
+ if (ts.isVariableDeclaration(node)) {
993
+ return Boolean(node.parent &&
994
+ ts.isVariableDeclarationList(node.parent) &&
995
+ node.parent.parent &&
996
+ ts.isVariableStatement(node.parent.parent) &&
997
+ ts.isSourceFile(node.parent.parent.parent));
998
+ }
999
+ return false;
1000
+ }
1001
+ function isExportedReusableDeclaration(node) {
1002
+ if (isExportedDeclaration(node))
1003
+ return true;
1004
+ if (ts.isMethodDeclaration(node)) {
1005
+ const parentClass = enclosingClassDeclaration(node);
1006
+ return Boolean(parentClass && isExportedDeclaration(parentClass));
1007
+ }
1008
+ return false;
1009
+ }
1010
+ function callableParameters(node) {
1011
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node))
1012
+ return Array.from(node.parameters);
1013
+ const initializer = functionLikeVariableInitializer(node);
1014
+ if (initializer)
1015
+ return Array.from(initializer.parameters);
1016
+ return [];
1017
+ }
1018
+ function callableReturnTypeNode(node) {
1019
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node))
1020
+ return node.type ?? null;
1021
+ const initializer = functionLikeVariableInitializer(node);
1022
+ if (initializer)
1023
+ return initializer.type ?? null;
1024
+ return null;
1025
+ }
1026
+ function propertyNameText(name) {
1027
+ if (!name)
1028
+ return null;
1029
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
1030
+ return name.text;
1031
+ if (ts.isPrivateIdentifier(name))
1032
+ return name.text;
1033
+ return null;
1034
+ }
1035
+ function typeShape(node) {
1036
+ if (!node)
1037
+ return 'implicit';
1038
+ switch (node.kind) {
1039
+ case ts.SyntaxKind.StringKeyword: return 'string';
1040
+ case ts.SyntaxKind.NumberKeyword: return 'number';
1041
+ case ts.SyntaxKind.BooleanKeyword: return 'boolean';
1042
+ case ts.SyntaxKind.VoidKeyword: return 'void';
1043
+ case ts.SyntaxKind.NullKeyword: return 'null';
1044
+ case ts.SyntaxKind.UndefinedKeyword: return 'undefined';
1045
+ case ts.SyntaxKind.AnyKeyword: return 'any';
1046
+ case ts.SyntaxKind.UnknownKeyword: return 'unknown';
1047
+ case ts.SyntaxKind.NeverKeyword: return 'never';
1048
+ case ts.SyntaxKind.ObjectKeyword: return 'object';
1049
+ case ts.SyntaxKind.ArrayType: return 'array';
1050
+ case ts.SyntaxKind.TupleType: return 'tuple';
1051
+ case ts.SyntaxKind.TypeLiteral: return 'object-literal';
1052
+ case ts.SyntaxKind.UnionType: return 'union';
1053
+ case ts.SyntaxKind.IntersectionType: return 'intersection';
1054
+ case ts.SyntaxKind.FunctionType: return 'function';
1055
+ case ts.SyntaxKind.LiteralType: return 'literal';
1056
+ case ts.SyntaxKind.ParenthesizedType: return ts.isParenthesizedTypeNode(node) ? typeShape(node.type) : 'parenthesized';
1057
+ case ts.SyntaxKind.TypePredicate: return 'predicate';
1058
+ default:
1059
+ if (ts.isTypeReferenceNode(node)) {
1060
+ const name = node.typeName.getText(node.getSourceFile()).toLowerCase();
1061
+ if (name === 'promise')
1062
+ return 'promise';
1063
+ if (name === 'array' || name === 'readonlyarray')
1064
+ return 'array';
1065
+ if (name === 'record' || name === 'map' || name === 'set' || name === 'weakmap' || name === 'weakset')
1066
+ return 'collection';
1067
+ return 'reference';
1068
+ }
1069
+ return 'other';
1070
+ }
1071
+ }
1072
+ function parameterShape(param) {
1073
+ const rest = param.dotDotDotToken ? 'rest' : 'value';
1074
+ const optional = param.questionToken || param.initializer ? 'optional' : 'required';
1075
+ return `${rest}:${optional}:${typeShape(param.type)}`;
1076
+ }
1077
+ function classMemberShapes(node) {
1078
+ const shapes = [];
1079
+ for (const member of node.members) {
1080
+ const name = propertyNameText(member.name) ?? 'anonymous';
1081
+ if (ts.isConstructorDeclaration(member)) {
1082
+ shapes.push(`constructor:${member.parameters.length}`);
1083
+ }
1084
+ else if (ts.isMethodDeclaration(member)) {
1085
+ shapes.push(`method:${name}:${member.parameters.length}:${typeShape(member.type)}`);
1086
+ }
1087
+ else if (ts.isPropertyDeclaration(member)) {
1088
+ shapes.push(`property:${name}:${typeShape(member.type)}`);
1089
+ }
1090
+ else if (ts.isGetAccessor(member)) {
1091
+ shapes.push(`get:${name}:${typeShape(member.type)}`);
1092
+ }
1093
+ else if (ts.isSetAccessor(member)) {
1094
+ shapes.push(`set:${name}:${member.parameters.length}`);
1095
+ }
1096
+ }
1097
+ return uniqueSorted(shapes);
1098
+ }
1099
+ function signatureShapeForDeclaration(node, kind) {
1100
+ if (kind === 'class' && ts.isClassDeclaration(node)) {
1101
+ const members = classMemberShapes(node);
1102
+ const constructor = node.members.find(ts.isConstructorDeclaration);
1103
+ const paramCount = constructor ? constructor.parameters.length : 0;
1104
+ return {
1105
+ shape: `class|ctor=${paramCount}|members=${members.join(',')}`,
1106
+ paramCount,
1107
+ memberCount: members.length,
1108
+ returnShape: null,
1109
+ };
1110
+ }
1111
+ const params = callableParameters(node);
1112
+ const returnShape = typeShape(callableReturnTypeNode(node) ?? undefined);
1113
+ return {
1114
+ shape: `${kind}|params=${params.map(parameterShape).join(',')}|return=${returnShape}`,
1115
+ paramCount: params.length,
1116
+ memberCount: 0,
1117
+ returnShape,
1118
+ };
1119
+ }
1120
+ function normalizedTokensForNode(node) {
1121
+ const sourceFile = node.getSourceFile();
1122
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, ts.LanguageVariant.Standard, node.getText(sourceFile));
1123
+ const tokens = [];
1124
+ let token = scanner.scan();
1125
+ while (token !== ts.SyntaxKind.EndOfFileToken) {
1126
+ if (token === ts.SyntaxKind.Identifier || token === ts.SyntaxKind.PrivateIdentifier) {
1127
+ tokens.push('ID');
1128
+ }
1129
+ else if (token === ts.SyntaxKind.StringLiteral ||
1130
+ token === ts.SyntaxKind.NumericLiteral ||
1131
+ token === ts.SyntaxKind.BigIntLiteral ||
1132
+ token === ts.SyntaxKind.RegularExpressionLiteral ||
1133
+ token === ts.SyntaxKind.FirstTemplateToken ||
1134
+ token === ts.SyntaxKind.TemplateHead ||
1135
+ token === ts.SyntaxKind.TemplateMiddle ||
1136
+ token === ts.SyntaxKind.TemplateTail) {
1137
+ tokens.push('LIT');
1138
+ }
1139
+ else {
1140
+ tokens.push(ts.tokenToString(token) ?? ts.SyntaxKind[token] ?? String(token));
1141
+ }
1142
+ token = scanner.scan();
1143
+ }
1144
+ return tokens;
1145
+ }
1146
+ function tokenShingles(tokens) {
1147
+ const out = new Set();
1148
+ if (tokens.length < REUSE_TOKEN_SHINGLE_SIZE)
1149
+ return out;
1150
+ for (let index = 0; index <= tokens.length - REUSE_TOKEN_SHINGLE_SIZE; index += 1) {
1151
+ out.add(hash(tokens.slice(index, index + REUSE_TOKEN_SHINGLE_SIZE).join(' '), 16));
1152
+ }
1153
+ return out;
1154
+ }
1155
+ function shingleOverlap(left, right) {
1156
+ if (left.size === 0 || right.size === 0)
1157
+ return 0;
1158
+ let intersection = 0;
1159
+ for (const item of left) {
1160
+ if (right.has(item))
1161
+ intersection += 1;
1162
+ }
1163
+ const union = left.size + right.size - intersection;
1164
+ return union > 0 ? intersection / union : 0;
1165
+ }
1166
+ function symbolRef(entry) {
1167
+ return {
1168
+ file: entry.file,
1169
+ name: entry.name,
1170
+ kind: entry.kind,
1171
+ exported: entry.exported,
1172
+ local: entry.local,
1173
+ signatureHash: entry.signatureHash,
1174
+ tokenFingerprintHash: entry.tokenFingerprintHash,
1175
+ tokenShingleSetHash: entry.tokenShingleSetHash,
1176
+ paramCount: entry.paramCount,
1177
+ memberCount: entry.memberCount,
1178
+ returnShape: entry.returnShape,
1179
+ provenance: 'repo-symbol-index',
1180
+ };
1181
+ }
1182
+ function reuseEntryFromDeclaration(decl, action) {
1183
+ const kind = reusableKind(decl.node);
1184
+ if (!kind)
1185
+ return null;
1186
+ const topLevel = isTopLevelReusableDeclaration(decl.node);
1187
+ const exported = isExportedReusableDeclaration(decl.node);
1188
+ const local = !exported;
1189
+ if (!exported && kind === 'method')
1190
+ return null;
1191
+ if (!exported && !topLevel && kind !== 'function')
1192
+ return null;
1193
+ const signature = signatureShapeForDeclaration(decl.node, kind);
1194
+ const normalizedTokens = normalizedTokensForNode(decl.node);
1195
+ const shingles = tokenShingles(normalizedTokens);
1196
+ const tokenFingerprintHash = normalizedTokens.length >= REUSE_MIN_TOKEN_COUNT
1197
+ ? hash(normalizedTokens.join(' '), 24)
1198
+ : null;
1199
+ const tokenShingleSetHash = shingles.size > 0 ? hash([...shingles].sort(), 24) : null;
1200
+ return {
1201
+ file: decl.file,
1202
+ name: decl.name,
1203
+ kind,
1204
+ exported,
1205
+ local,
1206
+ signatureHash: hash(signature.shape, 24),
1207
+ tokenFingerprintHash,
1208
+ tokenShingleSetHash,
1209
+ paramCount: signature.paramCount,
1210
+ memberCount: signature.memberCount,
1211
+ returnShape: signature.returnShape,
1212
+ provenance: 'repo-symbol-index',
1213
+ action,
1214
+ topLevel,
1215
+ signatureShape: signature.shape,
1216
+ normalizedTokens,
1217
+ tokenShingles: shingles,
1218
+ declaration: decl.node,
1219
+ };
1220
+ }
1221
+ function highInformationSignature(entry) {
1222
+ if (entry.kind === 'class')
1223
+ return entry.memberCount >= 2 || entry.paramCount >= 2;
1224
+ if (entry.paramCount >= 2)
1225
+ return true;
1226
+ if (entry.returnShape && !['implicit', 'void', 'any', 'unknown'].includes(entry.returnShape)) {
1227
+ return entry.paramCount >= 1;
1228
+ }
1229
+ return false;
1230
+ }
1231
+ function confidenceRank(confidence) {
1232
+ return confidence === 'high' ? 0 : 1;
1233
+ }
1234
+ function matchRank(matchType) {
1235
+ switch (matchType) {
1236
+ case 'signature_token_fingerprint_match': return 0;
1237
+ case 'token_fingerprint_match': return 1;
1238
+ case 'exported_name_collision': return 2;
1239
+ case 'normalized_signature_match': return 3;
1240
+ default: return 9;
1241
+ }
1242
+ }
1243
+ function mergeReuseFinding(current, next) {
1244
+ if (!current)
1245
+ return next;
1246
+ const reasonCodes = uniqueSorted([...current.reasonCodes, ...next.reasonCodes]);
1247
+ const tokenOverlap = Math.max(current.evidence.tokenOverlap ?? 0, next.evidence.tokenOverlap ?? 0);
1248
+ const signature = current.evidence.signatureHash ?? next.evidence.signatureHash;
1249
+ const tokenFingerprint = current.evidence.tokenFingerprintHash ?? next.evidence.tokenFingerprintHash;
1250
+ const tokenShingle = current.evidence.tokenShingleSetHash ?? next.evidence.tokenShingleSetHash;
1251
+ const matchType = reasonCodes.includes('same_normalized_signature') &&
1252
+ (reasonCodes.includes('same_normalized_token_fingerprint') || reasonCodes.includes('high_token_shingle_overlap'))
1253
+ ? 'signature_token_fingerprint_match'
1254
+ : reasonCodes.includes('same_normalized_token_fingerprint') || reasonCodes.includes('high_token_shingle_overlap')
1255
+ ? 'token_fingerprint_match'
1256
+ : reasonCodes.includes('exported_symbol_name_collision')
1257
+ ? 'exported_name_collision'
1258
+ : 'normalized_signature_match';
1259
+ const confidence = matchType === 'signature_token_fingerprint_match' ||
1260
+ reasonCodes.includes('same_normalized_token_fingerprint') ||
1261
+ reasonCodes.includes('exported_symbol_name_collision')
1262
+ ? 'high'
1263
+ : 'medium';
1264
+ return {
1265
+ ...current,
1266
+ matchType,
1267
+ confidence,
1268
+ reasonCodes,
1269
+ evidence: {
1270
+ ...current.evidence,
1271
+ signatureHash: signature,
1272
+ tokenFingerprintHash: tokenFingerprint,
1273
+ tokenShingleSetHash: tokenShingle,
1274
+ tokenOverlap: tokenOverlap > 0 ? Number(tokenOverlap.toFixed(3)) : null,
1275
+ },
1276
+ message: reuseFindingMessage(current.changed, current.existing, matchType),
1277
+ };
1278
+ }
1279
+ function reuseFindingMessage(changed, existing, matchType) {
1280
+ const subject = changed.kind === 'class' ? 'class' : changed.kind === 'method' ? 'method' : 'function';
1281
+ const match = matchType === 'signature_token_fingerprint_match'
1282
+ ? 'signature+token fingerprint'
1283
+ : matchType.replace(/_/g, ' ');
1284
+ return `Potential reuse issue: changed ${subject} ${changed.name} resembles existing ${existing.file}#${existing.name}; match type: ${match}; action: review existing helper before merging.`;
1285
+ }
1286
+ function reuseFindingForPair(changed, existing) {
1287
+ if (changed.file === existing.file)
1288
+ return null;
1289
+ const reasonCodes = [
1290
+ changed.action === 'add' ? 'changed_symbol_added' : 'changed_symbol_modified',
1291
+ 'existing_symbol_elsewhere',
1292
+ 'typescript_javascript_only',
1293
+ ];
1294
+ let signatureHash = null;
1295
+ let tokenFingerprintHash = null;
1296
+ let tokenShingleSetHash = null;
1297
+ let tokenOverlap = null;
1298
+ if (changed.exported &&
1299
+ existing.exported &&
1300
+ changed.topLevel &&
1301
+ existing.topLevel &&
1302
+ changed.kind === existing.kind &&
1303
+ changed.name === existing.name) {
1304
+ reasonCodes.push('exported_symbol_name_collision');
1305
+ }
1306
+ if (changed.kind === existing.kind &&
1307
+ changed.signatureHash === existing.signatureHash &&
1308
+ highInformationSignature(changed)) {
1309
+ reasonCodes.push('same_normalized_signature');
1310
+ signatureHash = changed.signatureHash;
1311
+ }
1312
+ if (changed.tokenFingerprintHash &&
1313
+ existing.tokenFingerprintHash &&
1314
+ changed.tokenFingerprintHash === existing.tokenFingerprintHash) {
1315
+ reasonCodes.push('same_normalized_token_fingerprint');
1316
+ tokenFingerprintHash = changed.tokenFingerprintHash;
1317
+ tokenShingleSetHash = changed.tokenShingleSetHash;
1318
+ tokenOverlap = 1;
1319
+ }
1320
+ else if (changed.normalizedTokens.length >= REUSE_MIN_TOKEN_COUNT &&
1321
+ existing.normalizedTokens.length >= REUSE_MIN_TOKEN_COUNT) {
1322
+ const overlap = shingleOverlap(changed.tokenShingles, existing.tokenShingles);
1323
+ if (overlap >= REUSE_TOKEN_OVERLAP_THRESHOLD) {
1324
+ reasonCodes.push('high_token_shingle_overlap');
1325
+ tokenShingleSetHash = changed.tokenShingleSetHash;
1326
+ tokenOverlap = Number(overlap.toFixed(3));
1327
+ }
1328
+ }
1329
+ if (!reasonCodes.includes('exported_symbol_name_collision') &&
1330
+ !reasonCodes.includes('same_normalized_signature') &&
1331
+ !reasonCodes.includes('same_normalized_token_fingerprint') &&
1332
+ !reasonCodes.includes('high_token_shingle_overlap')) {
1333
+ return null;
1334
+ }
1335
+ const matchType = reasonCodes.includes('same_normalized_signature') &&
1336
+ (reasonCodes.includes('same_normalized_token_fingerprint') || reasonCodes.includes('high_token_shingle_overlap'))
1337
+ ? 'signature_token_fingerprint_match'
1338
+ : reasonCodes.includes('same_normalized_token_fingerprint') || reasonCodes.includes('high_token_shingle_overlap')
1339
+ ? 'token_fingerprint_match'
1340
+ : reasonCodes.includes('exported_symbol_name_collision')
1341
+ ? 'exported_name_collision'
1342
+ : 'normalized_signature_match';
1343
+ const confidence = matchType === 'signature_token_fingerprint_match' ||
1344
+ reasonCodes.includes('same_normalized_token_fingerprint') ||
1345
+ reasonCodes.includes('exported_symbol_name_collision')
1346
+ ? 'high'
1347
+ : 'medium';
1348
+ const changedRef = symbolRef(changed);
1349
+ const existingRef = symbolRef(existing);
1350
+ return {
1351
+ schemaVersion: 'neurcode.reuse-finding.v1',
1352
+ severity: 'warn',
1353
+ advisory: true,
1354
+ hardBlock: false,
1355
+ changed: changedRef,
1356
+ existing: existingRef,
1357
+ matchType,
1358
+ confidence,
1359
+ reasonCodes: uniqueSorted(reasonCodes),
1360
+ evidence: {
1361
+ signatureHash,
1362
+ tokenFingerprintHash,
1363
+ tokenShingleSetHash,
1364
+ tokenOverlap,
1365
+ changedNormalizedTokenCount: changed.normalizedTokens.length,
1366
+ existingNormalizedTokenCount: existing.normalizedTokens.length,
1367
+ },
1368
+ action: 'review_existing_helper_before_merging',
1369
+ message: reuseFindingMessage(changedRef, existingRef, matchType),
1370
+ provenance: 'repo-symbol-index',
1371
+ };
1372
+ }
1373
+ function buildReuseDetection(input) {
1374
+ const entries = [];
1375
+ const changedActionByKey = new Map(input.targets.map((target) => [
1376
+ symbolKey(target.file, target.kind, target.name),
1377
+ target.action,
1378
+ ]));
1379
+ const changedCandidates = [];
1380
+ for (const declarations of input.declarationsByFile.values()) {
1381
+ for (const decl of declarations) {
1382
+ const key = symbolKey(decl.file, decl.kind, decl.name);
1383
+ const changedAction = changedActionByKey.get(key);
1384
+ if (changedAction === 'delete')
1385
+ continue;
1386
+ const entry = reuseEntryFromDeclaration(decl, changedAction ?? 'existing');
1387
+ if (!entry)
1388
+ continue;
1389
+ entries.push(entry);
1390
+ if (changedActionByKey.has(key))
1391
+ changedCandidates.push(entry);
1392
+ }
1393
+ }
1394
+ const byPair = new Map();
1395
+ for (const changed of changedCandidates) {
1396
+ for (const existing of entries) {
1397
+ if (existing === changed)
1398
+ continue;
1399
+ const finding = reuseFindingForPair(changed, existing);
1400
+ if (!finding)
1401
+ continue;
1402
+ const key = `${finding.changed.file}\0${finding.changed.name}\0${finding.existing.file}\0${finding.existing.name}`;
1403
+ byPair.set(key, mergeReuseFinding(byPair.get(key), finding));
1404
+ }
1405
+ }
1406
+ const sourceFileCount = input.program.getSourceFiles()
1407
+ .filter((file) => !file.isDeclarationFile && !file.fileName.includes('node_modules'))
1408
+ .length;
1409
+ const indexHash = hash(entries
1410
+ .map((entry) => ({
1411
+ file: entry.file,
1412
+ name: entry.name,
1413
+ kind: entry.kind,
1414
+ exported: entry.exported,
1415
+ signatureHash: entry.signatureHash,
1416
+ tokenFingerprintHash: entry.tokenFingerprintHash,
1417
+ tokenShingleSetHash: entry.tokenShingleSetHash,
1418
+ }))
1419
+ .sort((a, b) => a.file.localeCompare(b.file) ||
1420
+ a.name.localeCompare(b.name) ||
1421
+ a.kind.localeCompare(b.kind)));
1422
+ const reuseFindings = [...byPair.values()]
1423
+ .sort((a, b) => confidenceRank(a.confidence) - confidenceRank(b.confidence) ||
1424
+ matchRank(a.matchType) - matchRank(b.matchType) ||
1425
+ a.changed.file.localeCompare(b.changed.file) ||
1426
+ a.changed.name.localeCompare(b.changed.name) ||
1427
+ a.existing.file.localeCompare(b.existing.file) ||
1428
+ a.existing.name.localeCompare(b.existing.name))
1429
+ .slice(0, REUSE_MAX_FINDINGS);
1430
+ return {
1431
+ repoSymbolIndex: {
1432
+ schemaVersion: 'neurcode.repo-symbol-index.v1',
1433
+ language: 'typescript/javascript',
1434
+ modelUsed: false,
1435
+ sourceUploaded: false,
1436
+ sourceStored: false,
1437
+ indexedFileCount: sourceFileCount,
1438
+ indexedSymbolCount: entries.length,
1439
+ exportedSymbolCount: entries.filter((entry) => entry.exported).length,
1440
+ localFunctionCount: entries.filter((entry) => entry.local && entry.kind === 'function').length,
1441
+ methodCount: entries.filter((entry) => entry.kind === 'method').length,
1442
+ classCount: entries.filter((entry) => entry.kind === 'class').length,
1443
+ changedCandidateCount: changedCandidates.length,
1444
+ unsupportedLanguages: ['python'],
1445
+ indexHash,
1446
+ fingerprintAlgorithm: 'typescript-scanner-normalized-token-shingles-v1',
1447
+ signatureAlgorithm: 'name-insensitive-kind-arity-param-return-shape-v1',
1448
+ provenance: 'repo-symbol-index',
1449
+ },
1450
+ reuseFindings,
1451
+ };
1452
+ }
929
1453
  function smallestContaining(declarations, line) {
930
1454
  return declarations
931
1455
  .filter((decl) => line >= decl.startLine && line <= decl.endLine)
@@ -2243,6 +2767,11 @@ function buildStructuralUnderstanding(projectRoot, diffFiles, options = {}) {
2243
2767
  suppressedArtifacts,
2244
2768
  profile,
2245
2769
  });
2770
+ const reuseDetection = buildReuseDetection({
2771
+ program,
2772
+ declarationsByFile,
2773
+ targets: [...targets.values()],
2774
+ });
2246
2775
  const core = {
2247
2776
  schemaVersion: exports.STRUCTURAL_UNDERSTANDING_SCHEMA_VERSION,
2248
2777
  generatedAt,
@@ -2273,10 +2802,14 @@ function buildStructuralUnderstanding(projectRoot, diffFiles, options = {}) {
2273
2802
  testReferences,
2274
2803
  digest,
2275
2804
  consequenceUnderstanding,
2805
+ repoSymbolIndex: reuseDetection.repoSymbolIndex,
2806
+ reuseFindings: reuseDetection.reuseFindings,
2276
2807
  planAlignment,
2277
2808
  boundaryImpact,
2278
2809
  limitations: [
2279
2810
  ...commonLimitations(),
2811
+ 'Reuse governance is deterministic and TypeScript/JavaScript-first; Python and other languages are not enforced in V1.',
2812
+ 'Reuse findings are advisory WARN signals, not hard blocks; token fingerprints and normalized signatures can miss semantic duplicates or produce review noise.',
2280
2813
  ...(changedPackageNames.length
2281
2814
  ? [
2282
2815
  `Cross-package references resolved for direct importers of [${changedPackageNames.join(', ')}] across ${consumerPackages.length} consumer package(s). Transitive re-export chains and consumers that do not import the changed package directly may be under-counted.`,