@netgreener/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +28 -0
  3. package/TUTORIAL.md +495 -0
  4. package/dist/aggregator.d.ts +43 -0
  5. package/dist/aggregator.js +270 -0
  6. package/dist/analyze/analyzeGate.d.ts +44 -0
  7. package/dist/analyze/analyzeGate.js +114 -0
  8. package/dist/analyze/apiOverconsumptionDetector.d.ts +13 -0
  9. package/dist/analyze/apiOverconsumptionDetector.js +75 -0
  10. package/dist/analyze/astDetectors.d.ts +38 -0
  11. package/dist/analyze/astDetectors.js +566 -0
  12. package/dist/analyze/buildResourceFindings.d.ts +34 -0
  13. package/dist/analyze/buildResourceFindings.js +86 -0
  14. package/dist/analyze/cli.d.ts +20 -0
  15. package/dist/analyze/cli.js +186 -0
  16. package/dist/analyze/cpuResourceDetectors.d.ts +35 -0
  17. package/dist/analyze/cpuResourceDetectors.js +163 -0
  18. package/dist/analyze/heuristicScan.d.ts +61 -0
  19. package/dist/analyze/heuristicScan.js +91 -0
  20. package/dist/analyze/index.d.ts +15 -0
  21. package/dist/analyze/index.js +15 -0
  22. package/dist/analyze/loadTypescript.d.ts +8 -0
  23. package/dist/analyze/loadTypescript.js +23 -0
  24. package/dist/analyze/memoryIoDetectors.d.ts +15 -0
  25. package/dist/analyze/memoryIoDetectors.js +68 -0
  26. package/dist/analyze/preferAstDetectors.d.ts +18 -0
  27. package/dist/analyze/preferAstDetectors.js +91 -0
  28. package/dist/analyze/projectScan.d.ts +19 -0
  29. package/dist/analyze/projectScan.js +115 -0
  30. package/dist/analyze/resourceAdmission.d.ts +33 -0
  31. package/dist/analyze/resourceAdmission.js +91 -0
  32. package/dist/analyze/resourceFindingCandidate.d.ts +61 -0
  33. package/dist/analyze/resourceFindingCandidate.js +75 -0
  34. package/dist/analyze/retryAmplificationDetector.d.ts +11 -0
  35. package/dist/analyze/retryAmplificationDetector.js +55 -0
  36. package/dist/analyze/schemas/resource-finding.schema.json +352 -0
  37. package/dist/analyze/serviceDiscovery.d.ts +66 -0
  38. package/dist/analyze/serviceDiscovery.js +210 -0
  39. package/dist/analyze/unboundedParallelismDetector.d.ts +11 -0
  40. package/dist/analyze/unboundedParallelismDetector.js +57 -0
  41. package/dist/analyze/validateResourceFinding.d.ts +19 -0
  42. package/dist/analyze/validateResourceFinding.js +46 -0
  43. package/dist/bullmq.d.ts +33 -0
  44. package/dist/bullmq.js +106 -0
  45. package/dist/cli/helpText.d.ts +8 -0
  46. package/dist/cli/helpText.js +99 -0
  47. package/dist/cli/netgreener.d.ts +12 -0
  48. package/dist/cli/netgreener.js +65 -0
  49. package/dist/collectorIpc.d.ts +28 -0
  50. package/dist/collectorIpc.js +189 -0
  51. package/dist/collectorMetadata.d.ts +15 -0
  52. package/dist/collectorMetadata.js +33 -0
  53. package/dist/config.d.ts +41 -0
  54. package/dist/config.js +97 -0
  55. package/dist/contract/index.d.ts +1 -0
  56. package/dist/contract/index.js +1 -0
  57. package/dist/contract/observation-envelope.schema.json +331 -0
  58. package/dist/contract/observation-protobuf-view.schema.json +773 -0
  59. package/dist/contract/observation-v1.d.mts +42 -0
  60. package/dist/contract/observation-v1.mjs +555 -0
  61. package/dist/contract/observationIdentity.d.ts +11 -0
  62. package/dist/contract/observationIdentity.js +48 -0
  63. package/dist/contract/observationProjection.d.ts +3 -0
  64. package/dist/contract/observationProjection.js +29 -0
  65. package/dist/contract/validate.d.ts +20 -0
  66. package/dist/contract/validate.js +227 -0
  67. package/dist/dogfood/mp3WorkerGates.d.ts +54 -0
  68. package/dist/dogfood/mp3WorkerGates.js +86 -0
  69. package/dist/dogfood/retainDryRunArtifact.d.ts +45 -0
  70. package/dist/dogfood/retainDryRunArtifact.js +103 -0
  71. package/dist/dogfood/tenantDogfoodGates.d.ts +76 -0
  72. package/dist/dogfood/tenantDogfoodGates.js +206 -0
  73. package/dist/exporter.d.ts +56 -0
  74. package/dist/exporter.js +304 -0
  75. package/dist/express.d.ts +44 -0
  76. package/dist/express.js +118 -0
  77. package/dist/externalApiMeter.d.ts +72 -0
  78. package/dist/externalApiMeter.js +1167 -0
  79. package/dist/fastify.d.ts +53 -0
  80. package/dist/fastify.js +148 -0
  81. package/dist/index.d.ts +32 -0
  82. package/dist/index.js +31 -0
  83. package/dist/n4/deploymentMatrix.d.ts +22 -0
  84. package/dist/n4/deploymentMatrix.js +57 -0
  85. package/dist/n4/missingnessInventory.d.ts +22 -0
  86. package/dist/n4/missingnessInventory.js +72 -0
  87. package/dist/nest.d.ts +42 -0
  88. package/dist/nest.js +87 -0
  89. package/dist/processResources.d.ts +28 -0
  90. package/dist/processResources.js +31 -0
  91. package/dist/processRuntime.d.ts +35 -0
  92. package/dist/processRuntime.js +96 -0
  93. package/dist/requestContext.d.ts +21 -0
  94. package/dist/requestContext.js +33 -0
  95. package/dist/runtime.d.ts +60 -0
  96. package/dist/runtime.js +289 -0
  97. package/dist/runtimeHealth.d.ts +35 -0
  98. package/dist/runtimeHealth.js +121 -0
  99. package/dist/serverlessHints.d.ts +26 -0
  100. package/dist/serverlessHints.js +52 -0
  101. package/dist/tenantContext.d.ts +78 -0
  102. package/dist/tenantContext.js +226 -0
  103. package/dist/tenantDefaults.d.ts +9 -0
  104. package/dist/tenantDefaults.js +9 -0
  105. package/dist/types.d.ts +131 -0
  106. package/dist/types.js +2 -0
  107. package/dist/uploader.d.ts +23 -0
  108. package/dist/uploader.js +52 -0
  109. package/dist/windowId.d.ts +2 -0
  110. package/dist/windowId.js +7 -0
  111. package/examples/analyze-manifest.mjs +30 -0
  112. package/examples/bullmq-live-smoke.mjs +178 -0
  113. package/examples/bullmq-mp3-worker-gate.mjs +237 -0
  114. package/examples/express-dry-run.mjs +72 -0
  115. package/examples/process-mp3-restart-gate.mjs +135 -0
  116. package/examples/retain-dry-run.mjs +110 -0
  117. package/examples/tenant-dogfood.mjs +269 -0
  118. package/fixtures/analyze-sample/architectureNoise.ts +13 -0
  119. package/fixtures/analyze-sample/cpuHotPaths.ts +27 -0
  120. package/fixtures/analyze-sample/fanOutClient.ts +15 -0
  121. package/fixtures/analyze-sample/inferenceHotPaths.ts +22 -0
  122. package/fixtures/analyze-sample/memoryIoHotPaths.ts +21 -0
  123. package/fixtures/analyze-sample/modelLoadHotPaths.ts +11 -0
  124. package/fixtures/analyze-sample/nestedLookup.ts +15 -0
  125. package/fixtures/analyze-sample/retryClient.ts +10 -0
  126. package/fixtures/analyze-sample/securitySmell.ts +8 -0
  127. package/fixtures/analyze-sample/server.ts +11 -0
  128. package/fixtures/analyze-sample/styleOnly.ts +7 -0
  129. package/fixtures/analyze-sample/worker.ts +4 -0
  130. package/fixtures/dual-view-projection.v1.json +53 -0
  131. package/fixtures/identity-preimages.v1.json +42 -0
  132. package/fixtures/mp2_shadow_digest_golden.v1.json +47 -0
  133. package/fixtures/projection-batch-document.v1.json +170 -0
  134. package/fixtures/projection-batch.v1.json +9 -0
  135. package/fixtures/python_reference_shapes.md +24 -0
  136. package/fixtures/session_metadata_runtime_v0.json +120 -0
  137. package/fixtures/session_metadata_with_tenant.json +85 -0
  138. package/package.json +137 -0
@@ -0,0 +1,566 @@
1
+ /**
2
+ * TypeScript AST detectors for Analyze (Python AST parity path).
3
+ *
4
+ * Returns ``null`` when typescript is unavailable or the file cannot be parsed —
5
+ * callers must fall back to heuristic detectors.
6
+ */
7
+ import { languageForPath, } from './resourceFindingCandidate.js';
8
+ import { tryLoadTypescript } from './loadTypescript.js';
9
+ const DETECTOR_VERSION = '0.1.0';
10
+ const RETRY_NAME = /\b(?:attempt|retry|tries|retries|maxRetries|max_retries)\b/i;
11
+ const CAP_HINT_RE = /\b(?:pLimit|p-limit|bottleneck|Semaphore|mapLimit|eachLimit|PromisePool|concurrency\s*:)\b/i;
12
+ const API_ROOTS = new Set([
13
+ 'fetch',
14
+ 'axios',
15
+ 'got',
16
+ 'request',
17
+ 'openai',
18
+ 'anthropic',
19
+ 'stripe',
20
+ 'cohere',
21
+ 'mistral',
22
+ 'groq',
23
+ 'replicate',
24
+ 'huggingface',
25
+ 'together',
26
+ 'bedrock',
27
+ 'sagemaker',
28
+ 'twilio',
29
+ 'sendgrid',
30
+ 'pinecone',
31
+ ]);
32
+ const FILE_READ_NAMES = new Set(['readFile', 'readFileSync']);
33
+ const COMP_METHODS = new Set(['map', 'flatMap', 'filter', 'forEach']);
34
+ const SUBPROCESS_NAMES = new Set([
35
+ 'exec',
36
+ 'execSync',
37
+ 'spawn',
38
+ 'spawnSync',
39
+ 'fork',
40
+ 'execFile',
41
+ 'execFileSync',
42
+ ]);
43
+ const SLEEP_NAMES = new Set(['setTimeout', 'setInterval']);
44
+ /** Nested-loop per-item scans (Python unvectorized_nested_loops twin). */
45
+ const LOOKUP_METHODS = new Set(['find', 'includes', 'indexOf', 'lastIndexOf']);
46
+ /** Explicit model/runtime loaders (Python model_load_in_loop twin). */
47
+ const MODEL_LOAD_NAMES = new Set([
48
+ 'loadLayersModel',
49
+ 'loadGraphModel',
50
+ 'from_pretrained',
51
+ 'loadModel',
52
+ ]);
53
+ /** Explicit per-item inference (Python inference_inside_loop twin). */
54
+ const INFERENCE_NAMES = new Set([
55
+ 'predict',
56
+ 'predictOnBatch',
57
+ 'executeAsync',
58
+ 'forward',
59
+ ]);
60
+ const HEAVY_MODEL_PACKAGES = new Set([
61
+ '@tensorflow/tfjs',
62
+ '@tensorflow/tfjs-node',
63
+ 'onnxruntime-node',
64
+ 'onnxruntime-web',
65
+ 'onnxruntime',
66
+ '@xenova/transformers',
67
+ '@huggingface/transformers',
68
+ ]);
69
+ function emptyBundle() {
70
+ return {
71
+ retry: [],
72
+ fileReads: [],
73
+ overconsumption: [],
74
+ unbounded: [],
75
+ polling: [],
76
+ repeatedWork: [],
77
+ processChurn: [],
78
+ fullDataset: [],
79
+ perItem: [],
80
+ modelLoad: [],
81
+ inference: [],
82
+ };
83
+ }
84
+ function scriptKindForPath(ts, relPath) {
85
+ const lower = relPath.toLowerCase();
86
+ if (lower.endsWith('.tsx'))
87
+ return ts.ScriptKind.TSX;
88
+ if (lower.endsWith('.jsx'))
89
+ return ts.ScriptKind.JSX;
90
+ if (lower.endsWith('.ts'))
91
+ return ts.ScriptKind.TS;
92
+ return ts.ScriptKind.JS;
93
+ }
94
+ function calleeName(ts, expr) {
95
+ if (ts.isIdentifier(expr))
96
+ return expr.text;
97
+ if (ts.isPropertyAccessExpression(expr)) {
98
+ const right = expr.name.text;
99
+ if (ts.isIdentifier(expr.expression)) {
100
+ return `${expr.expression.text}.${right}`;
101
+ }
102
+ return right;
103
+ }
104
+ return '';
105
+ }
106
+ /** Leftmost identifier in a property chain (``groq.chat.completions.create`` → ``groq``). */
107
+ function rootIdentifier(ts, expr) {
108
+ let cur = expr;
109
+ while (ts.isPropertyAccessExpression(cur) || ts.isParenthesizedExpression(cur)) {
110
+ cur = ts.isPropertyAccessExpression(cur) ? cur.expression : cur.expression;
111
+ }
112
+ return ts.isIdentifier(cur) ? cur.text : '';
113
+ }
114
+ function leafName(name) {
115
+ return name.includes('.') ? name.split('.').pop() || name : name;
116
+ }
117
+ function looksLikeApiCall(ts, expr) {
118
+ const name = calleeName(ts, expr);
119
+ if (name) {
120
+ const root = name.split('.')[0] || name;
121
+ if (API_ROOTS.has(root) || API_ROOTS.has(name))
122
+ return true;
123
+ }
124
+ const chainRoot = rootIdentifier(ts, expr);
125
+ return Boolean(chainRoot) && API_ROOTS.has(chainRoot);
126
+ }
127
+ function looksLikeFileRead(name) {
128
+ if (!name)
129
+ return false;
130
+ const leaf = leafName(name);
131
+ return FILE_READ_NAMES.has(leaf) || FILE_READ_NAMES.has(name);
132
+ }
133
+ function looksLikePromiseAll(name) {
134
+ return name === 'Promise.all' || name === 'Promise.allSettled' || name.endsWith('.all');
135
+ }
136
+ function looksLikeSleep(name) {
137
+ return SLEEP_NAMES.has(name) || name === 'Bun.sleep' || name.endsWith('.wait');
138
+ }
139
+ function looksLikeSubprocess(name) {
140
+ return SUBPROCESS_NAMES.has(leafName(name));
141
+ }
142
+ function looksLikeNestedLookup(name) {
143
+ return LOOKUP_METHODS.has(leafName(name));
144
+ }
145
+ function looksLikeModelLoadCall(name) {
146
+ if (!name)
147
+ return false;
148
+ const leaf = leafName(name);
149
+ if (MODEL_LOAD_NAMES.has(leaf))
150
+ return true;
151
+ // onnxruntime: InferenceSession.create
152
+ return name === 'InferenceSession.create' || name.endsWith('.InferenceSession.create');
153
+ }
154
+ function looksLikeInferenceCall(name) {
155
+ if (!name)
156
+ return false;
157
+ const leaf = leafName(name);
158
+ if (INFERENCE_NAMES.has(leaf))
159
+ return true;
160
+ // ONNX Runtime session.run / ort.run — require session-ish qualifier
161
+ if (leaf === 'run') {
162
+ const lower = name.toLowerCase();
163
+ return (lower.includes('session') ||
164
+ lower === 'ort.run' ||
165
+ lower.endsWith('.ort.run'));
166
+ }
167
+ return false;
168
+ }
169
+ function inferenceHit(path, line, language) {
170
+ return hit(path, line, language, {
171
+ mechanism_id: 'per_item_instead_of_batch',
172
+ primary_domain: 'compute.gpu',
173
+ admission_domain: 'compute.gpu',
174
+ title: 'Model inference inside loop may run per item instead of a supported batch path',
175
+ detector_id: 'js.ast.inference_inside_loop',
176
+ legacy_issue_type: 'inference_inside_loop',
177
+ });
178
+ }
179
+ function stringLiteralValue(ts, expr) {
180
+ if (!expr)
181
+ return null;
182
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
183
+ return expr.text;
184
+ }
185
+ return null;
186
+ }
187
+ function isHeavyModelPackage(spec) {
188
+ if (!spec)
189
+ return false;
190
+ return HEAVY_MODEL_PACKAGES.has(spec);
191
+ }
192
+ function modelLoadHit(path, line, language) {
193
+ return hit(path, line, language, {
194
+ mechanism_id: 'process_or_runtime_churn',
195
+ primary_domain: 'compute.cpu',
196
+ admission_domain: 'compute.cpu',
197
+ title: 'Model/runtime load inside loop may repeat expensive initialization',
198
+ detector_id: 'js.ast.model_load_in_loop',
199
+ legacy_issue_type: 'model_load_in_loop',
200
+ });
201
+ }
202
+ function loopHeaderLooksRetry(ts, node, sf) {
203
+ if (ts.isForStatement(node)) {
204
+ const parts = [node.initializer, node.condition, node.incrementor]
205
+ .filter(Boolean)
206
+ .map((p) => p.getText(sf))
207
+ .join(' ');
208
+ return RETRY_NAME.test(parts);
209
+ }
210
+ if (ts.isWhileStatement(node) || ts.isDoStatement(node)) {
211
+ return RETRY_NAME.test(node.expression.getText(sf));
212
+ }
213
+ if (ts.isForOfStatement(node) || ts.isForInStatement(node)) {
214
+ return RETRY_NAME.test(node.getText(sf).slice(0, 120));
215
+ }
216
+ return false;
217
+ }
218
+ function parseSource(ts, source, relPath) {
219
+ try {
220
+ return ts.createSourceFile(relPath.replace(/\\/g, '/'), source, ts.ScriptTarget.Latest, true, scriptKindForPath(ts, relPath));
221
+ }
222
+ catch {
223
+ return null;
224
+ }
225
+ }
226
+ function containsApiCall(ts, node) {
227
+ let found = false;
228
+ const walk = (n) => {
229
+ if (found)
230
+ return;
231
+ if (ts.isCallExpression(n) && looksLikeApiCall(ts, n.expression)) {
232
+ found = true;
233
+ return;
234
+ }
235
+ ts.forEachChild(n, walk);
236
+ };
237
+ walk(node);
238
+ return found;
239
+ }
240
+ function hit(path, line, language, opts) {
241
+ return {
242
+ mechanism_id: opts.mechanism_id,
243
+ primary_domain: opts.primary_domain,
244
+ admission_domain: opts.admission_domain,
245
+ title: opts.title,
246
+ rel_path: path,
247
+ start_line: line,
248
+ end_line: line,
249
+ language,
250
+ detector_id: opts.detector_id,
251
+ detector_version: DETECTOR_VERSION,
252
+ legacy_issue_type: opts.legacy_issue_type,
253
+ };
254
+ }
255
+ function looksLikeJsonParse(name) {
256
+ return name === 'JSON.parse';
257
+ }
258
+ function unwrapExpr(ts, expr) {
259
+ let cur = expr;
260
+ while (true) {
261
+ if (ts.isAwaitExpression(cur)) {
262
+ cur = cur.expression;
263
+ continue;
264
+ }
265
+ if (ts.isParenthesizedExpression(cur)) {
266
+ cur = cur.expression;
267
+ continue;
268
+ }
269
+ if (ts.isAsExpression(cur)) {
270
+ cur = cur.expression;
271
+ continue;
272
+ }
273
+ break;
274
+ }
275
+ return cur;
276
+ }
277
+ function isFileReadCall(ts, expr) {
278
+ const inner = unwrapExpr(ts, expr);
279
+ return ts.isCallExpression(inner) && looksLikeFileRead(calleeName(ts, inner.expression));
280
+ }
281
+ function fullDatasetHit(path, line, language) {
282
+ return hit(path, line, language, {
283
+ mechanism_id: 'memory_retention_or_materialization',
284
+ primary_domain: 'memory.host',
285
+ admission_domain: 'memory.host',
286
+ title: 'Loading and parsing an entire file may retain a full dataset in memory',
287
+ detector_id: 'js.ast.full_dataset_in_memory',
288
+ legacy_issue_type: 'full_dataset_in_memory',
289
+ });
290
+ }
291
+ function walkHits(ts, sf, relPath) {
292
+ const language = languageForPath(relPath);
293
+ const path = relPath.replace(/\\/g, '/');
294
+ const out = emptyBundle();
295
+ const stack = [];
296
+ let comprehensionDepth = 0;
297
+ let promiseAllDepth = 0;
298
+ /** Recent ``const x = readFile…`` bindings for JSON.parse(x) pairing. */
299
+ const recentFileReads = [];
300
+ const mentionsChildProcess = /\b(?:node:)?child_process\b/.test(sf.text) ||
301
+ /\bfrom\s+['"](?:node:)?child_process['"]/.test(sf.text);
302
+ const rememberFileReadBinding = (name, line) => {
303
+ recentFileReads.push({ name, line });
304
+ if (recentFileReads.length > 8)
305
+ recentFileReads.shift();
306
+ };
307
+ const visit = (node) => {
308
+ const isLoop = ts.isForStatement(node) ||
309
+ ts.isForOfStatement(node) ||
310
+ ts.isForInStatement(node) ||
311
+ ts.isWhileStatement(node) ||
312
+ ts.isDoStatement(node);
313
+ if (isLoop) {
314
+ const kind = loopHeaderLooksRetry(ts, node, sf)
315
+ ? 'retry'
316
+ : 'plain';
317
+ stack.push({ kind });
318
+ ts.forEachChild(node, visit);
319
+ stack.pop();
320
+ return;
321
+ }
322
+ if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) && node.initializer) {
323
+ if (isFileReadCall(ts, node.initializer)) {
324
+ const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
325
+ rememberFileReadBinding(node.name.text, line);
326
+ }
327
+ }
328
+ if (ts.isCallExpression(node)) {
329
+ const name = calleeName(ts, node.expression);
330
+ const leaf = leafName(name);
331
+ const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
332
+ const inLoop = stack.length > 0;
333
+ const inRetry = stack.some((f) => f.kind === 'retry');
334
+ if (looksLikeJsonParse(name) && node.arguments.length > 0) {
335
+ const arg = unwrapExpr(ts, node.arguments[0]);
336
+ if (ts.isCallExpression(arg) && looksLikeFileRead(calleeName(ts, arg.expression))) {
337
+ out.fullDataset.push(fullDatasetHit(path, line, language));
338
+ }
339
+ else if (ts.isIdentifier(arg)) {
340
+ const prior = [...recentFileReads].reverse().find((r) => r.name === arg.text);
341
+ if (prior) {
342
+ out.fullDataset.push(fullDatasetHit(path, prior.line, language));
343
+ }
344
+ }
345
+ }
346
+ if (looksLikePromiseAll(name)) {
347
+ const argText = node.arguments.map((a) => a.getText(sf)).join(' ');
348
+ if (!CAP_HINT_RE.test(argText) &&
349
+ (/\.map\s*\(/.test(argText) || /\.flatMap\s*\(/.test(argText)) &&
350
+ containsApiCall(ts, node)) {
351
+ out.unbounded.push(hit(path, line, language, {
352
+ mechanism_id: 'unbounded_parallelism',
353
+ primary_domain: 'network.io',
354
+ admission_domain: 'network.io',
355
+ title: 'Unbounded Promise.all fan-out may spike concurrent I/O cost',
356
+ detector_id: 'js.ast.unbounded_promise_all',
357
+ legacy_issue_type: 'unbounded_asyncio_gather_with_io',
358
+ }));
359
+ }
360
+ promiseAllDepth += 1;
361
+ ts.forEachChild(node, visit);
362
+ promiseAllDepth -= 1;
363
+ return;
364
+ }
365
+ if (COMP_METHODS.has(leaf)) {
366
+ comprehensionDepth += 1;
367
+ ts.forEachChild(node, visit);
368
+ comprehensionDepth -= 1;
369
+ return;
370
+ }
371
+ if (looksLikeApiCall(ts, node.expression)) {
372
+ if (inRetry) {
373
+ out.retry.push(hit(path, line, language, {
374
+ mechanism_id: 'retry_amplification',
375
+ primary_domain: 'external_api',
376
+ admission_domain: 'external_api',
377
+ title: 'Retry loop may amplify billed API attempts',
378
+ detector_id: 'js.ast.retry_loop',
379
+ legacy_issue_type: 'api_call_in_retry_loop',
380
+ }));
381
+ }
382
+ else if (comprehensionDepth > 0 && promiseAllDepth === 0) {
383
+ out.overconsumption.push(hit(path, line, language, {
384
+ mechanism_id: 'external_api_overconsumption',
385
+ primary_domain: 'external_api',
386
+ admission_domain: 'external_api',
387
+ title: 'API call inside map/filter may multiply billed attempts',
388
+ detector_id: 'js.ast.api_in_comprehension',
389
+ legacy_issue_type: 'api_call_in_comprehension',
390
+ }));
391
+ }
392
+ else if (inLoop && !inRetry) {
393
+ out.overconsumption.push(hit(path, line, language, {
394
+ mechanism_id: 'external_api_overconsumption',
395
+ primary_domain: 'external_api',
396
+ admission_domain: 'external_api',
397
+ title: 'API call inside loop may multiply billed attempts',
398
+ detector_id: 'js.ast.api_in_loop',
399
+ legacy_issue_type: 'network_call_in_loop',
400
+ }));
401
+ }
402
+ }
403
+ if (inLoop && looksLikeFileRead(name)) {
404
+ out.fileReads.push(hit(path, line, language, {
405
+ mechanism_id: 'repeated_work',
406
+ primary_domain: 'storage.io',
407
+ admission_domain: 'storage.io',
408
+ title: 'File read inside loop may repeat avoidable I/O work',
409
+ detector_id: 'js.ast.repeated_file_reads',
410
+ legacy_issue_type: 'repeated_file_reads',
411
+ }));
412
+ }
413
+ if (inLoop && looksLikeSleep(name)) {
414
+ out.polling.push(hit(path, line, language, {
415
+ mechanism_id: 'polling_or_idle_work',
416
+ primary_domain: 'compute.cpu',
417
+ admission_domain: 'compute.cpu',
418
+ title: 'Sleep/delay inside loop may burn CPU with idle wakeups',
419
+ detector_id: 'js.ast.sleep_polling',
420
+ legacy_issue_type: 'sleep_polling',
421
+ }));
422
+ }
423
+ if (inLoop && looksLikeSubprocess(name)) {
424
+ const strong = /(?:Sync|fork)$/.test(leaf) || mentionsChildProcess || leaf === 'fork';
425
+ if (strong) {
426
+ out.processChurn.push(hit(path, line, language, {
427
+ mechanism_id: 'process_or_runtime_churn',
428
+ primary_domain: 'compute.cpu',
429
+ admission_domain: 'compute.cpu',
430
+ title: 'Subprocess spawn/exec inside loop may repeat process churn',
431
+ detector_id: 'js.ast.subprocess_in_loop',
432
+ legacy_issue_type: 'subprocess_in_loop',
433
+ }));
434
+ }
435
+ }
436
+ if (stack.length >= 2 && looksLikeNestedLookup(name)) {
437
+ out.perItem.push(hit(path, line, language, {
438
+ mechanism_id: 'per_item_instead_of_batch',
439
+ primary_domain: 'compute.cpu',
440
+ admission_domain: 'compute.cpu',
441
+ title: 'Nested-loop array lookup may scan per item instead of a batched/indexed path',
442
+ detector_id: 'js.ast.unvectorized_nested_loops',
443
+ legacy_issue_type: 'unvectorized_nested_loops',
444
+ }));
445
+ }
446
+ if (inLoop && looksLikeModelLoadCall(name)) {
447
+ out.modelLoad.push(modelLoadHit(path, line, language));
448
+ }
449
+ if (inLoop && looksLikeInferenceCall(name)) {
450
+ out.inference.push(inferenceHit(path, line, language));
451
+ }
452
+ if (inLoop && leaf === 'require' && node.arguments.length > 0) {
453
+ const spec = stringLiteralValue(ts, node.arguments[0]);
454
+ if (isHeavyModelPackage(spec)) {
455
+ out.modelLoad.push(modelLoadHit(path, line, language));
456
+ }
457
+ }
458
+ if (inLoop &&
459
+ node.expression.kind === ts.SyntaxKind.ImportKeyword &&
460
+ node.arguments.length > 0) {
461
+ const spec = stringLiteralValue(ts, node.arguments[0]);
462
+ if (isHeavyModelPackage(spec)) {
463
+ out.modelLoad.push(modelLoadHit(path, line, language));
464
+ }
465
+ }
466
+ }
467
+ if (ts.isNewExpression(node) && stack.length > 0) {
468
+ const ctor = node.expression.getText(sf);
469
+ if (ctor === 'RegExp') {
470
+ const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
471
+ out.repeatedWork.push(hit(path, line, language, {
472
+ mechanism_id: 'repeated_work',
473
+ primary_domain: 'compute.cpu',
474
+ admission_domain: 'compute.cpu',
475
+ title: 'RegExp compiled inside loop may repeat avoidable CPU work',
476
+ detector_id: 'js.ast.regex_compile_in_loop',
477
+ legacy_issue_type: 'regex_compile_in_loop',
478
+ }));
479
+ }
480
+ }
481
+ if (ts.isBinaryExpression(node) && stack.length > 0) {
482
+ if (node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken) {
483
+ const right = node.right;
484
+ const isStringy = ts.isStringLiteral(right) ||
485
+ ts.isNoSubstitutionTemplateLiteral(right) ||
486
+ ts.isTemplateExpression(right) ||
487
+ (ts.isCallExpression(right) && calleeName(ts, right.expression) === 'String');
488
+ if (isStringy) {
489
+ const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
490
+ out.repeatedWork.push(hit(path, line, language, {
491
+ mechanism_id: 'repeated_work',
492
+ primary_domain: 'compute.cpu',
493
+ admission_domain: 'compute.cpu',
494
+ title: 'String concatenation inside loop may repeat avoidable CPU work',
495
+ detector_id: 'js.ast.string_concat_in_loop',
496
+ legacy_issue_type: 'string_concat_in_loop',
497
+ }));
498
+ }
499
+ }
500
+ }
501
+ ts.forEachChild(node, visit);
502
+ };
503
+ visit(sf);
504
+ return out;
505
+ }
506
+ /** Full AST hit bundle, or ``null`` to signal heuristic fallback. */
507
+ export function detectAllHitsAst(source, relPath) {
508
+ const ts = tryLoadTypescript();
509
+ if (!ts)
510
+ return null;
511
+ const sf = parseSource(ts, source, relPath);
512
+ if (!sf)
513
+ return null;
514
+ return walkHits(ts, sf, relPath);
515
+ }
516
+ /** @deprecated Prefer detectAllHitsAst — kept for callers. */
517
+ export function detectHotPathHitsAst(source, relPath) {
518
+ const all = detectAllHitsAst(source, relPath);
519
+ if (!all)
520
+ return null;
521
+ return { retry: all.retry, fileReads: all.fileReads };
522
+ }
523
+ export function detectRetryAmplificationHitsAst(source, relPath) {
524
+ const all = detectAllHitsAst(source, relPath);
525
+ return all ? all.retry : null;
526
+ }
527
+ export function detectRepeatedFileReadHitsAst(source, relPath) {
528
+ const all = detectAllHitsAst(source, relPath);
529
+ return all ? all.fileReads : null;
530
+ }
531
+ export function detectApiOverconsumptionHitsAst(source, relPath) {
532
+ const all = detectAllHitsAst(source, relPath);
533
+ return all ? all.overconsumption : null;
534
+ }
535
+ export function detectUnboundedParallelismHitsAst(source, relPath) {
536
+ const all = detectAllHitsAst(source, relPath);
537
+ return all ? all.unbounded : null;
538
+ }
539
+ export function detectPollingIdleHitsAst(source, relPath) {
540
+ const all = detectAllHitsAst(source, relPath);
541
+ return all ? all.polling : null;
542
+ }
543
+ export function detectRepeatedWorkHitsAst(source, relPath) {
544
+ const all = detectAllHitsAst(source, relPath);
545
+ return all ? all.repeatedWork : null;
546
+ }
547
+ export function detectProcessChurnHitsAst(source, relPath) {
548
+ const all = detectAllHitsAst(source, relPath);
549
+ return all ? all.processChurn : null;
550
+ }
551
+ export function detectFullDatasetInMemoryHitsAst(source, relPath) {
552
+ const all = detectAllHitsAst(source, relPath);
553
+ return all ? all.fullDataset : null;
554
+ }
555
+ export function detectPerItemInsteadOfBatchHitsAst(source, relPath) {
556
+ const all = detectAllHitsAst(source, relPath);
557
+ return all ? all.perItem : null;
558
+ }
559
+ export function detectModelLoadInLoopHitsAst(source, relPath) {
560
+ const all = detectAllHitsAst(source, relPath);
561
+ return all ? all.modelLoad : null;
562
+ }
563
+ export function detectInferenceInsideLoopHitsAst(source, relPath) {
564
+ const all = detectAllHitsAst(source, relPath);
565
+ return all ? all.inference : null;
566
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Orchestrate resource-candidate emission from in-memory sources or disk.
3
+ * Prefer TypeScript AST when available; heuristics otherwise.
4
+ * Additive Analyze path — never touches RunSession upload.
5
+ */
6
+ import { type ListCandidateSourceFilesOptions } from './projectScan.js';
7
+ import { type ResourceFindingCandidateV1 } from './resourceFindingCandidate.js';
8
+ import type { ServiceManifestV0 } from './serviceDiscovery.js';
9
+ export type BuildResourceFindingsOptions = ListCandidateSourceFilesOptions & {
10
+ projectId?: string;
11
+ /** Max candidate findings to return (default 50). */
12
+ maxFindings?: number;
13
+ };
14
+ export type ResourceAnalyzeResultV0 = {
15
+ schema: 'resource_analyze_result_v0';
16
+ contract_status: 'mp4_scaffold';
17
+ project_id: string;
18
+ manifest: ServiceManifestV0;
19
+ findings: ResourceFindingCandidateV1[];
20
+ stats: {
21
+ files_scanned: number;
22
+ findings: number;
23
+ by_mechanism: Record<string, number>;
24
+ };
25
+ notes: string[];
26
+ };
27
+ /**
28
+ * Build admitted ``resource_finding_v1`` candidates from path→source map.
29
+ */
30
+ export declare function buildResourceFindingsFromSources(sources: Record<string, string>, opts?: BuildResourceFindingsOptions): ResourceFindingCandidateV1[];
31
+ /**
32
+ * Walk project (or explicit paths), emit candidates + service manifest together.
33
+ */
34
+ export declare function buildResourceAnalyzeResultFromPaths(projectDir: string, relPaths?: Iterable<string> | null, opts?: BuildResourceFindingsOptions): ResourceAnalyzeResultV0;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Orchestrate resource-candidate emission from in-memory sources or disk.
3
+ * Prefer TypeScript AST when available; heuristics otherwise.
4
+ * Additive Analyze path — never touches RunSession upload.
5
+ */
6
+ import { readFileSync, statSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { collectPreferAstHits } from './preferAstDetectors.js';
9
+ import { buildServiceManifestFromPaths, listCandidateSourceFiles, } from './projectScan.js';
10
+ import { toResourceFindingCandidate, } from './resourceFindingCandidate.js';
11
+ import { filterValidResourceFindings } from './validateResourceFinding.js';
12
+ import { isTypescriptAnalyzeAvailable } from './loadTypescript.js';
13
+ function collectHitsFromSources(sources) {
14
+ const hits = [];
15
+ for (const [rel, source] of Object.entries(sources)) {
16
+ hits.push(...collectPreferAstHits(source, rel));
17
+ }
18
+ return hits;
19
+ }
20
+ /**
21
+ * Build admitted ``resource_finding_v1`` candidates from path→source map.
22
+ */
23
+ export function buildResourceFindingsFromSources(sources, opts = {}) {
24
+ const maxFindings = Math.max(1, opts.maxFindings ?? 50);
25
+ const projectId = opts.projectId ?? 'local';
26
+ const out = [];
27
+ for (const hit of collectHitsFromSources(sources)) {
28
+ const finding = toResourceFindingCandidate(hit, projectId);
29
+ if (!finding)
30
+ continue;
31
+ out.push(finding);
32
+ if (out.length >= maxFindings)
33
+ break;
34
+ }
35
+ return filterValidResourceFindings(out);
36
+ }
37
+ /**
38
+ * Walk project (or explicit paths), emit candidates + service manifest together.
39
+ */
40
+ export function buildResourceAnalyzeResultFromPaths(projectDir, relPaths, opts = {}) {
41
+ const projectId = opts.projectId ?? 'local';
42
+ const maxFileBytes = Math.max(1024, opts.maxFileBytes ?? 512 * 1024);
43
+ const paths = relPaths
44
+ ? [...relPaths].filter(Boolean).map((p) => String(p).replace(/\\/g, '/'))
45
+ : listCandidateSourceFiles(projectDir, opts);
46
+ const sources = {};
47
+ for (const rel of paths) {
48
+ try {
49
+ const abs = join(projectDir, rel);
50
+ const st = statSync(abs);
51
+ if (!st.isFile() || st.size > maxFileBytes)
52
+ continue;
53
+ sources[rel.replace(/\\/g, '/')] = readFileSync(abs, { encoding: 'utf8' });
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ }
59
+ const findings = buildResourceFindingsFromSources(sources, opts);
60
+ const by_mechanism = {};
61
+ for (const f of findings) {
62
+ by_mechanism[f.mechanism_id] = (by_mechanism[f.mechanism_id] || 0) + 1;
63
+ }
64
+ const manifest = buildServiceManifestFromPaths(projectDir, Object.keys(sources), opts);
65
+ return {
66
+ schema: 'resource_analyze_result_v0',
67
+ contract_status: 'mp4_scaffold',
68
+ project_id: projectId,
69
+ manifest,
70
+ findings,
71
+ stats: {
72
+ files_scanned: Object.keys(sources).length,
73
+ findings: findings.length,
74
+ by_mechanism,
75
+ },
76
+ notes: [
77
+ 'MP4 scaffold: TypeScript AST preferred for all current RD0 twins; heuristics fallback when typescript absent',
78
+ 'Stable mechanism_id/evidence_state aligned to resource_finding_v1 + RD0 disposition',
79
+ 'Does not modify RunSession upload, CLI, or VSCE wiring',
80
+ 'Detectors: retry_amplification, external_api_overconsumption, unbounded_parallelism, polling_or_idle_work, repeated_work, process_or_runtime_churn (subprocess + model_load), memory_retention_or_materialization, per_item_instead_of_batch (nested lookup + inference_inside_loop)',
81
+ `typescript AST available=${isTypescriptAnalyzeAvailable()}`,
82
+ 'Candidates are fail-closed filtered through vendored resource_finding_v1 JSON Schema (Ajv)',
83
+ 'Skipped RD0 context-only: http_call_without_timeout; skipped low-energy print_in_loop; skipped ML-only excess_training_work / accelerator_underutilization',
84
+ ],
85
+ };
86
+ }