@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,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Thin Node Analyze CLI — print ``service_manifest_v0`` + ``resource_finding_v1``
4
+ * candidates for a project path. Never uploads; never touches RunSession.
5
+ *
6
+ * npx netgreener-analyze ./my-app
7
+ * npm run example:analyze-cli -- ./fixtures/analyze-sample
8
+ *
9
+ * Options:
10
+ * --project-id <id> project_id stamped on candidates (default: local)
11
+ * --paths a.ts,b.ts explicit repo-relative paths (skip auto-walk)
12
+ * --findings-only print findings array only
13
+ * --manifest-only print service manifest only
14
+ * --json-summary print machine-readable gate summary only (CI)
15
+ * --fail-on any|<mechanism>[,…] CI gate (exit 1 when exceeded)
16
+ * --max-at-or-above <n> with --fail-on: allow up to N matches (default 0)
17
+ * --quiet suppress non-JSON status lines on stderr
18
+ */
19
+ /** Run analyze subcommand; ``argv`` is args after ``analyze`` / bin name. */
20
+ export declare function runAnalyzeCli(argv: string[]): number;
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Thin Node Analyze CLI — print ``service_manifest_v0`` + ``resource_finding_v1``
4
+ * candidates for a project path. Never uploads; never touches RunSession.
5
+ *
6
+ * npx netgreener-analyze ./my-app
7
+ * npm run example:analyze-cli -- ./fixtures/analyze-sample
8
+ *
9
+ * Options:
10
+ * --project-id <id> project_id stamped on candidates (default: local)
11
+ * --paths a.ts,b.ts explicit repo-relative paths (skip auto-walk)
12
+ * --findings-only print findings array only
13
+ * --manifest-only print service manifest only
14
+ * --json-summary print machine-readable gate summary only (CI)
15
+ * --fail-on any|<mechanism>[,…] CI gate (exit 1 when exceeded)
16
+ * --max-at-or-above <n> with --fail-on: allow up to N matches (default 0)
17
+ * --quiet suppress non-JSON status lines on stderr
18
+ */
19
+ import { accessSync, constants, statSync } from 'node:fs';
20
+ import { resolve } from 'node:path';
21
+ import { pathToFileURL } from 'node:url';
22
+ import { buildAnalyzeGateSummary, findingsExceedFailThreshold, formatAnalyzeGateFailureMessage, resolveAnalyzeGateLimit, } from './analyzeGate.js';
23
+ import { buildResourceAnalyzeResultFromPaths } from './buildResourceFindings.js';
24
+ import { formatAnalyzeHelp } from '../cli/helpText.js';
25
+ function printHelp() {
26
+ process.stdout.write(formatAnalyzeHelp());
27
+ }
28
+ function parseArgs(argv) {
29
+ const flags = {
30
+ projectId: 'local',
31
+ paths: null,
32
+ findingsOnly: false,
33
+ manifestOnly: false,
34
+ jsonSummary: false,
35
+ failOn: null,
36
+ maxAtOrAbove: null,
37
+ quiet: false,
38
+ help: false,
39
+ positional: [],
40
+ };
41
+ for (let i = 0; i < argv.length; i += 1) {
42
+ const arg = argv[i] ?? '';
43
+ if (arg === '-h' || arg === '--help') {
44
+ flags.help = true;
45
+ continue;
46
+ }
47
+ if (arg === '--findings-only') {
48
+ flags.findingsOnly = true;
49
+ continue;
50
+ }
51
+ if (arg === '--manifest-only') {
52
+ flags.manifestOnly = true;
53
+ continue;
54
+ }
55
+ if (arg === '--json-summary') {
56
+ flags.jsonSummary = true;
57
+ continue;
58
+ }
59
+ if (arg === '--quiet') {
60
+ flags.quiet = true;
61
+ continue;
62
+ }
63
+ if (arg === '--project-id') {
64
+ flags.projectId = String(argv[++i] || 'local');
65
+ continue;
66
+ }
67
+ if (arg.startsWith('--project-id=')) {
68
+ flags.projectId = arg.slice('--project-id='.length) || 'local';
69
+ continue;
70
+ }
71
+ if (arg === '--paths') {
72
+ const raw = String(argv[++i] || '');
73
+ flags.paths = raw
74
+ .split(',')
75
+ .map((p) => p.trim())
76
+ .filter(Boolean);
77
+ continue;
78
+ }
79
+ if (arg.startsWith('--paths=')) {
80
+ flags.paths = arg
81
+ .slice('--paths='.length)
82
+ .split(',')
83
+ .map((p) => p.trim())
84
+ .filter(Boolean);
85
+ continue;
86
+ }
87
+ if (arg === '--fail-on') {
88
+ flags.failOn = String(argv[++i] || '').trim() || null;
89
+ continue;
90
+ }
91
+ if (arg.startsWith('--fail-on=')) {
92
+ flags.failOn = arg.slice('--fail-on='.length).trim() || null;
93
+ continue;
94
+ }
95
+ if (arg === '--max-at-or-above') {
96
+ flags.maxAtOrAbove = Number(argv[++i]);
97
+ continue;
98
+ }
99
+ if (arg.startsWith('--max-at-or-above=')) {
100
+ flags.maxAtOrAbove = Number(arg.slice('--max-at-or-above='.length));
101
+ continue;
102
+ }
103
+ if (arg.startsWith('-')) {
104
+ throw new Error(`Unknown option: ${arg}`);
105
+ }
106
+ flags.positional.push(arg);
107
+ }
108
+ return flags;
109
+ }
110
+ /** Run analyze subcommand; ``argv`` is args after ``analyze`` / bin name. */
111
+ export function runAnalyzeCli(argv) {
112
+ let flags;
113
+ try {
114
+ flags = parseArgs(argv);
115
+ }
116
+ catch (err) {
117
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
118
+ printHelp();
119
+ return 2;
120
+ }
121
+ if (flags.help || flags.positional.length === 0) {
122
+ printHelp();
123
+ return flags.help ? 0 : 2;
124
+ }
125
+ const modeCount = [flags.findingsOnly, flags.manifestOnly, flags.jsonSummary].filter(Boolean)
126
+ .length;
127
+ if (modeCount > 1) {
128
+ process.stderr.write('Choose at most one of --findings-only / --manifest-only / --json-summary\n');
129
+ return 2;
130
+ }
131
+ if (flags.maxAtOrAbove != null && flags.failOn == null) {
132
+ process.stderr.write('--max-at-or-above requires --fail-on\n');
133
+ return 2;
134
+ }
135
+ if (flags.maxAtOrAbove != null && (!Number.isFinite(flags.maxAtOrAbove) || flags.maxAtOrAbove < 0)) {
136
+ process.stderr.write('--max-at-or-above must be a non-negative number\n');
137
+ return 2;
138
+ }
139
+ const projectDir = resolve(flags.positional[0]);
140
+ try {
141
+ accessSync(projectDir, constants.R_OK);
142
+ if (!statSync(projectDir).isDirectory()) {
143
+ throw new Error('not a directory');
144
+ }
145
+ }
146
+ catch {
147
+ process.stderr.write(`Project path is not a readable directory: ${projectDir}\n`);
148
+ return 2;
149
+ }
150
+ const result = buildResourceAnalyzeResultFromPaths(projectDir, flags.paths, {
151
+ projectId: flags.projectId,
152
+ });
153
+ const summary = buildAnalyzeGateSummary({
154
+ projectId: flags.projectId,
155
+ filesScanned: result.stats.files_scanned,
156
+ findings: result.findings,
157
+ failOn: flags.failOn,
158
+ maxAtOrAbove: flags.maxAtOrAbove,
159
+ });
160
+ const payload = flags.jsonSummary
161
+ ? summary
162
+ : flags.findingsOnly
163
+ ? result.findings
164
+ : flags.manifestOnly
165
+ ? result.manifest
166
+ : result;
167
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
168
+ if (!flags.quiet && !flags.jsonSummary) {
169
+ process.stderr.write(`netgreener analyze: files=${result.stats.files_scanned} findings=${result.stats.findings} upload=never\n`);
170
+ }
171
+ const limit = resolveAnalyzeGateLimit(flags.failOn, flags.maxAtOrAbove);
172
+ if (flags.failOn && limit != null) {
173
+ if (findingsExceedFailThreshold(result.findings, flags.failOn, limit)) {
174
+ if (!flags.jsonSummary) {
175
+ process.stderr.write(`${formatAnalyzeGateFailureMessage(result.findings, flags.failOn, limit)}\n`);
176
+ }
177
+ return 1;
178
+ }
179
+ }
180
+ return summary.exit_code;
181
+ }
182
+ const invokedDirectly = typeof process.argv[1] === 'string' &&
183
+ pathToFileURL(resolve(process.argv[1])).href === import.meta.url;
184
+ if (invokedDirectly) {
185
+ process.exitCode = runAnalyzeCli(process.argv.slice(2));
186
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Heuristic JS/TS twins for local CPU-ish RD0 candidate_map rules:
3
+ * - sleep_polling → polling_or_idle_work / compute.cpu
4
+ * - regex_compile_in_loop / string_concat_in_loop → repeated_work / compute.cpu
5
+ * - subprocess_in_loop → process_or_runtime_churn / compute.cpu
6
+ * - model_load_in_loop → process_or_runtime_churn / compute.cpu
7
+ * - unvectorized_nested_loops → per_item_instead_of_batch / compute.cpu
8
+ * - inference_inside_loop → per_item_instead_of_batch / compute.gpu
9
+ *
10
+ * Skips low-energy ``print_in_loop`` (Python always-downgrade class).
11
+ * Skips ML-only training gates (``no_early_stopping`` / ``no_lr_scheduler`` /
12
+ * ``no_mixed_precision`` → excess_training_work / accelerator_underutilization).
13
+ */
14
+ import { type ResourceCandidateHit } from './resourceFindingCandidate.js';
15
+ /** Python ``sleep_polling`` twin. */
16
+ export declare function detectPollingIdleHits(source: string, relPath: string): ResourceCandidateHit[];
17
+ /** Python ``regex_compile_in_loop`` + ``string_concat_in_loop`` twins. */
18
+ export declare function detectRepeatedWorkHits(source: string, relPath: string): ResourceCandidateHit[];
19
+ /** Python ``subprocess_in_loop`` twin (child_process spawn/exec). */
20
+ export declare function detectProcessChurnHits(source: string, relPath: string): ResourceCandidateHit[];
21
+ /**
22
+ * Python ``model_load_in_loop`` twin (TF.js / ONNX / transformers loaders,
23
+ * or dynamic import/require of heavy model packages inside a loop).
24
+ */
25
+ export declare function detectModelLoadInLoopHits(source: string, relPath: string): ResourceCandidateHit[];
26
+ /**
27
+ * Python ``unvectorized_nested_loops`` twin: array lookup inside nested loops
28
+ * (O(n²) per-item scan instead of a batched/indexed path).
29
+ */
30
+ export declare function detectPerItemInsteadOfBatchHits(source: string, relPath: string): ResourceCandidateHit[];
31
+ /**
32
+ * Python ``inference_inside_loop`` twin (TF.js predict / ONNX session.run /
33
+ * executeAsync inside a loop → per-item instead of batch; compute.gpu).
34
+ */
35
+ export declare function detectInferenceInsideLoopHits(source: string, relPath: string): ResourceCandidateHit[];
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Heuristic JS/TS twins for local CPU-ish RD0 candidate_map rules:
3
+ * - sleep_polling → polling_or_idle_work / compute.cpu
4
+ * - regex_compile_in_loop / string_concat_in_loop → repeated_work / compute.cpu
5
+ * - subprocess_in_loop → process_or_runtime_churn / compute.cpu
6
+ * - model_load_in_loop → process_or_runtime_churn / compute.cpu
7
+ * - unvectorized_nested_loops → per_item_instead_of_batch / compute.cpu
8
+ * - inference_inside_loop → per_item_instead_of_batch / compute.gpu
9
+ *
10
+ * Skips low-energy ``print_in_loop`` (Python always-downgrade class).
11
+ * Skips ML-only training gates (``no_early_stopping`` / ``no_lr_scheduler`` /
12
+ * ``no_mixed_precision`` → excess_training_work / accelerator_underutilization).
13
+ */
14
+ import { forEachSourceLine, INFERENCE_RE, MODEL_LOAD_RE, NESTED_LOOKUP_RE, REGEX_COMPILE_RE, SLEEP_POLL_RE, STRING_CONCAT_RE, SUBPROCESS_RE, } from './heuristicScan.js';
15
+ import { languageForPath, } from './resourceFindingCandidate.js';
16
+ const DETECTOR_VERSION = '0.1.0';
17
+ function baseHit(relPath, lineNo, language, opts) {
18
+ const primary = opts.primary_domain ?? 'compute.cpu';
19
+ return {
20
+ mechanism_id: opts.mechanism_id,
21
+ primary_domain: primary,
22
+ admission_domain: primary,
23
+ title: opts.title,
24
+ rel_path: relPath,
25
+ start_line: lineNo,
26
+ end_line: lineNo,
27
+ language,
28
+ detector_id: opts.detector_id,
29
+ detector_version: DETECTOR_VERSION,
30
+ legacy_issue_type: opts.legacy_issue_type,
31
+ };
32
+ }
33
+ /** Python ``sleep_polling`` twin. */
34
+ export function detectPollingIdleHits(source, relPath) {
35
+ const hits = [];
36
+ const language = languageForPath(relPath);
37
+ const path = relPath.replace(/\\/g, '/');
38
+ forEachSourceLine(source, ({ line, lineNo, inLoop }) => {
39
+ if (!inLoop || !SLEEP_POLL_RE.test(line))
40
+ return;
41
+ hits.push(baseHit(path, lineNo, language, {
42
+ mechanism_id: 'polling_or_idle_work',
43
+ title: 'Sleep/delay inside loop may burn CPU with idle wakeups',
44
+ detector_id: 'js.static.sleep_polling',
45
+ legacy_issue_type: 'sleep_polling',
46
+ }));
47
+ });
48
+ return hits;
49
+ }
50
+ /** Python ``regex_compile_in_loop`` + ``string_concat_in_loop`` twins. */
51
+ export function detectRepeatedWorkHits(source, relPath) {
52
+ const hits = [];
53
+ const language = languageForPath(relPath);
54
+ const path = relPath.replace(/\\/g, '/');
55
+ forEachSourceLine(source, ({ line, lineNo, inLoop }) => {
56
+ if (!inLoop)
57
+ return;
58
+ if (REGEX_COMPILE_RE.test(line)) {
59
+ hits.push(baseHit(path, lineNo, language, {
60
+ mechanism_id: 'repeated_work',
61
+ title: 'RegExp compiled inside loop may repeat avoidable CPU work',
62
+ detector_id: 'js.static.regex_compile_in_loop',
63
+ legacy_issue_type: 'regex_compile_in_loop',
64
+ }));
65
+ }
66
+ if (STRING_CONCAT_RE.test(line)) {
67
+ hits.push(baseHit(path, lineNo, language, {
68
+ mechanism_id: 'repeated_work',
69
+ title: 'String concatenation inside loop may repeat avoidable CPU work',
70
+ detector_id: 'js.static.string_concat_in_loop',
71
+ legacy_issue_type: 'string_concat_in_loop',
72
+ }));
73
+ }
74
+ });
75
+ return hits;
76
+ }
77
+ /** Python ``subprocess_in_loop`` twin (child_process spawn/exec). */
78
+ export function detectProcessChurnHits(source, relPath) {
79
+ const hits = [];
80
+ const language = languageForPath(relPath);
81
+ const path = relPath.replace(/\\/g, '/');
82
+ const mentionsChildProcess = /\bchild_process\b|\bnode:child_process\b/.test(source) ||
83
+ /\bfrom\s+['"](?:node:)?child_process['"]/.test(source) ||
84
+ /\brequire\(\s*['"](?:node:)?child_process['"]\s*\)/.test(source);
85
+ forEachSourceLine(source, ({ line, lineNo, inLoop }) => {
86
+ if (!inLoop || !SUBPROCESS_RE.test(line))
87
+ return;
88
+ // Avoid flagging unrelated ``spawn`` names unless child_process is in file,
89
+ // except execSync/spawnSync which are strongly Node subprocess APIs.
90
+ const strong = /\b(?:execSync|spawnSync|execFileSync|fork)\s*\(/.test(line) ||
91
+ mentionsChildProcess;
92
+ if (!strong)
93
+ return;
94
+ hits.push(baseHit(path, lineNo, language, {
95
+ mechanism_id: 'process_or_runtime_churn',
96
+ title: 'Subprocess spawn/exec inside loop may repeat process churn',
97
+ detector_id: 'js.static.subprocess_in_loop',
98
+ legacy_issue_type: 'subprocess_in_loop',
99
+ }));
100
+ });
101
+ return hits;
102
+ }
103
+ /**
104
+ * Python ``model_load_in_loop`` twin (TF.js / ONNX / transformers loaders,
105
+ * or dynamic import/require of heavy model packages inside a loop).
106
+ */
107
+ export function detectModelLoadInLoopHits(source, relPath) {
108
+ const hits = [];
109
+ const language = languageForPath(relPath);
110
+ const path = relPath.replace(/\\/g, '/');
111
+ forEachSourceLine(source, ({ line, lineNo, inLoop }) => {
112
+ if (!inLoop || !MODEL_LOAD_RE.test(line))
113
+ return;
114
+ hits.push(baseHit(path, lineNo, language, {
115
+ mechanism_id: 'process_or_runtime_churn',
116
+ title: 'Model/runtime load inside loop may repeat expensive initialization',
117
+ detector_id: 'js.static.model_load_in_loop',
118
+ legacy_issue_type: 'model_load_in_loop',
119
+ }));
120
+ });
121
+ return hits;
122
+ }
123
+ /**
124
+ * Python ``unvectorized_nested_loops`` twin: array lookup inside nested loops
125
+ * (O(n²) per-item scan instead of a batched/indexed path).
126
+ */
127
+ export function detectPerItemInsteadOfBatchHits(source, relPath) {
128
+ const hits = [];
129
+ const language = languageForPath(relPath);
130
+ const path = relPath.replace(/\\/g, '/');
131
+ forEachSourceLine(source, ({ line, lineNo, loopDepth }) => {
132
+ if (loopDepth < 2 || !NESTED_LOOKUP_RE.test(line))
133
+ return;
134
+ hits.push(baseHit(path, lineNo, language, {
135
+ mechanism_id: 'per_item_instead_of_batch',
136
+ title: 'Nested-loop array lookup may scan per item instead of a batched/indexed path',
137
+ detector_id: 'js.static.unvectorized_nested_loops',
138
+ legacy_issue_type: 'unvectorized_nested_loops',
139
+ }));
140
+ });
141
+ return hits;
142
+ }
143
+ /**
144
+ * Python ``inference_inside_loop`` twin (TF.js predict / ONNX session.run /
145
+ * executeAsync inside a loop → per-item instead of batch; compute.gpu).
146
+ */
147
+ export function detectInferenceInsideLoopHits(source, relPath) {
148
+ const hits = [];
149
+ const language = languageForPath(relPath);
150
+ const path = relPath.replace(/\\/g, '/');
151
+ forEachSourceLine(source, ({ line, lineNo, inLoop }) => {
152
+ if (!inLoop || !INFERENCE_RE.test(line))
153
+ return;
154
+ hits.push(baseHit(path, lineNo, language, {
155
+ mechanism_id: 'per_item_instead_of_batch',
156
+ primary_domain: 'compute.gpu',
157
+ title: 'Model inference inside loop may run per item instead of a supported batch path',
158
+ detector_id: 'js.static.inference_inside_loop',
159
+ legacy_issue_type: 'inference_inside_loop',
160
+ }));
161
+ });
162
+ return hits;
163
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared heuristic helpers for MP4 JS/TS resource detectors.
3
+ * Not a TypeScript AST — line + brace-depth only.
4
+ */
5
+ /** HTTP / paid API call sites commonly metered as external_api. */
6
+ export declare const API_CALL_RE: RegExp;
7
+ /** Retry-ish for/while headers (Python attempt|retry|tries twin). */
8
+ export declare const RETRY_HEADER_RE: RegExp;
9
+ /** Plain for/while loop header (any). */
10
+ export declare const LOOP_HEADER_RE: RegExp;
11
+ /** Array iteration that behaves like a Python comprehension for fan-out cost. */
12
+ export declare const COMPREHENSION_RE: RegExp;
13
+ /** Unbounded concurrent fan-out (JS twin of asyncio.gather). */
14
+ export declare const PROMISE_ALL_RE: RegExp;
15
+ /** Sleep / delay polling (Python sleep_polling twin). */
16
+ export declare const SLEEP_POLL_RE: RegExp;
17
+ /** RegExp compiled inside a hot path (Python regex_compile_in_loop twin). */
18
+ export declare const REGEX_COMPILE_RE: RegExp;
19
+ /** String concat growth in a loop (Python string_concat_in_loop twin). */
20
+ export declare const STRING_CONCAT_RE: RegExp;
21
+ /** child_process style spawn/exec (Python subprocess_in_loop twin). */
22
+ export declare const SUBPROCESS_RE: RegExp;
23
+ /** Whole-file reads (fs) — loop reuse → repeated_file_reads. */
24
+ export declare const FILE_READ_RE: RegExp;
25
+ /**
26
+ * Per-item array lookup (JS twin of Python ``unvectorized_nested_loops``).
27
+ * Meaningful when nested loop depth ≥ 2.
28
+ */
29
+ export declare const NESTED_LOOKUP_RE: RegExp;
30
+ /**
31
+ * Model / runtime load inside a loop (Python ``model_load_in_loop`` twin).
32
+ * Prefer explicit loader APIs; avoid bare ``pipeline(`` (Node streams).
33
+ */
34
+ export declare const MODEL_LOAD_RE: RegExp;
35
+ /**
36
+ * Per-item model inference inside a loop (Python ``inference_inside_loop`` twin).
37
+ * Prefer explicit predict/execute/session.run; avoid bare ``.run(`` / Node streams.
38
+ */
39
+ export declare const INFERENCE_RE: RegExp;
40
+ export declare function stripLineComment(line: string): string;
41
+ export declare function hasApiCall(line: string): boolean;
42
+ /**
43
+ * Walk lines updating brace depth; returns depth after processing ``line``.
44
+ * Also reports whether an opening `{` occurred on this line.
45
+ */
46
+ export declare function advanceBraceDepth(line: string, braceDepth: number): {
47
+ braceDepth: number;
48
+ openedBrace: boolean;
49
+ };
50
+ export type LoopBodyVisitor = (input: {
51
+ line: string;
52
+ lineNo: number;
53
+ inLoop: boolean;
54
+ /** Nested for/while depth (same-line `{` headers only, matching prior heuristic). */
55
+ loopDepth: number;
56
+ }) => void;
57
+ /**
58
+ * Invoke ``visit`` for each line; ``inLoop`` is true inside for/while bodies.
59
+ * Tracks nested loop depth when headers open a brace on the same line.
60
+ */
61
+ export declare function forEachSourceLine(source: string, visit: LoopBodyVisitor): void;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Shared heuristic helpers for MP4 JS/TS resource detectors.
3
+ * Not a TypeScript AST — line + brace-depth only.
4
+ */
5
+ /** HTTP / paid API call sites commonly metered as external_api. */
6
+ export const API_CALL_RE = /\b(?:fetch|axios(?:\.(?:get|post|put|delete|patch|request|head|options))?|got|request|openai|anthropic|stripe|cohere|mistral|groq|replicate|huggingface|together|bedrock|sagemaker|twilio|sendgrid|pinecone|@aws-sdk)\b|\bhttps?\.(?:get|request)\s*\(/i;
7
+ /** Retry-ish for/while headers (Python attempt|retry|tries twin). */
8
+ export const RETRY_HEADER_RE = /^\s*(?:for|while)\s*\([^;)]*(?:\battempt\b|\bretry\b|\btries\b|\bretries\b|\bmaxRetries\b|\bmax_retries\b)[^)]*\)/i;
9
+ /** Plain for/while loop header (any). */
10
+ export const LOOP_HEADER_RE = /^\s*(?:for|while)\s*\(/i;
11
+ /** Array iteration that behaves like a Python comprehension for fan-out cost. */
12
+ export const COMPREHENSION_RE = /\.(?:map|flatMap|filter|forEach)\s*\(/i;
13
+ /** Unbounded concurrent fan-out (JS twin of asyncio.gather). */
14
+ export const PROMISE_ALL_RE = /\bPromise\.all(?:Settled)?\s*\(/i;
15
+ /** Sleep / delay polling (Python sleep_polling twin). */
16
+ export const SLEEP_POLL_RE = /\b(?:setTimeout|setInterval|Atomics\.wait)\s*\(|\b(?:Bun\.sleep|await\s+(?:delay|sleep|wait)\s*\()/i;
17
+ /** RegExp compiled inside a hot path (Python regex_compile_in_loop twin). */
18
+ export const REGEX_COMPILE_RE = /\bnew\s+RegExp\s*\(/;
19
+ /** String concat growth in a loop (Python string_concat_in_loop twin). */
20
+ export const STRING_CONCAT_RE = /\w+\s*\+=\s*(?:['"`]|String\s*\()/;
21
+ /** child_process style spawn/exec (Python subprocess_in_loop twin). */
22
+ export const SUBPROCESS_RE = /\b(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?|fork)\s*\(/;
23
+ /** Whole-file reads (fs) — loop reuse → repeated_file_reads. */
24
+ export const FILE_READ_RE = /\b(?:readFileSync|readFile)\s*\(|\bfs\.promises\.readFile\s*\(|\bpromises\.readFile\s*\(/;
25
+ /**
26
+ * Per-item array lookup (JS twin of Python ``unvectorized_nested_loops``).
27
+ * Meaningful when nested loop depth ≥ 2.
28
+ */
29
+ export const NESTED_LOOKUP_RE = /\.\s*(?:find|includes|indexOf|lastIndexOf)\s*\(/;
30
+ /**
31
+ * Model / runtime load inside a loop (Python ``model_load_in_loop`` twin).
32
+ * Prefer explicit loader APIs; avoid bare ``pipeline(`` (Node streams).
33
+ */
34
+ export const MODEL_LOAD_RE = /\b(?:loadLayersModel|loadGraphModel|from_pretrained|loadModel)\s*\(|\bInferenceSession\.create\s*\(|\b(?:tf|ort)\.(?:loadLayersModel|loadGraphModel|InferenceSession)\b|\brequire\(\s*['"](?:@tensorflow\/(?:tfjs(?:-node)?)|onnxruntime(?:-node|-web)?|@xenova\/transformers|@huggingface\/transformers)['"]\s*\)|\bimport\(\s*['"](?:@tensorflow\/(?:tfjs(?:-node)?)|onnxruntime(?:-node|-web)?|@xenova\/transformers|@huggingface\/transformers)['"]\s*\)/;
35
+ /**
36
+ * Per-item model inference inside a loop (Python ``inference_inside_loop`` twin).
37
+ * Prefer explicit predict/execute/session.run; avoid bare ``.run(`` / Node streams.
38
+ */
39
+ export const INFERENCE_RE = /\.\s*(?:predict(?:OnBatch)?|executeAsync|forward)\s*\(|\b(?:session|ortSession|inferenceSession|ort)\.run\s*\(/i;
40
+ export function stripLineComment(line) {
41
+ const idx = line.indexOf('//');
42
+ return idx >= 0 ? line.slice(0, idx) : line;
43
+ }
44
+ export function hasApiCall(line) {
45
+ return API_CALL_RE.test(line);
46
+ }
47
+ /**
48
+ * Walk lines updating brace depth; returns depth after processing ``line``.
49
+ * Also reports whether an opening `{` occurred on this line.
50
+ */
51
+ export function advanceBraceDepth(line, braceDepth) {
52
+ let depth = braceDepth;
53
+ let openedBrace = false;
54
+ for (let c = 0; c < line.length; c += 1) {
55
+ const ch = line[c];
56
+ if (ch === '{') {
57
+ depth += 1;
58
+ openedBrace = true;
59
+ }
60
+ else if (ch === '}') {
61
+ depth = Math.max(0, depth - 1);
62
+ }
63
+ }
64
+ return { braceDepth: depth, openedBrace };
65
+ }
66
+ /**
67
+ * Invoke ``visit`` for each line; ``inLoop`` is true inside for/while bodies.
68
+ * Tracks nested loop depth when headers open a brace on the same line.
69
+ */
70
+ export function forEachSourceLine(source, visit) {
71
+ const lines = source.split(/\r?\n/);
72
+ let braceDepth = 0;
73
+ /** Exit thresholds: leave loop when braceDepth ≤ entry. */
74
+ const loopStack = [];
75
+ for (let i = 0; i < lines.length; i += 1) {
76
+ const line = stripLineComment(lines[i] ?? '');
77
+ const lineNo = i + 1;
78
+ const isLoopHeader = LOOP_HEADER_RE.test(line);
79
+ const before = braceDepth;
80
+ const { braceDepth: next, openedBrace } = advanceBraceDepth(line, braceDepth);
81
+ if (isLoopHeader && openedBrace) {
82
+ loopStack.push(before);
83
+ }
84
+ braceDepth = next;
85
+ while (loopStack.length > 0 && braceDepth <= loopStack[loopStack.length - 1]) {
86
+ loopStack.pop();
87
+ }
88
+ const loopDepth = loopStack.length;
89
+ visit({ line, lineNo, inLoop: loopDepth > 0, loopDepth });
90
+ }
91
+ }
@@ -0,0 +1,15 @@
1
+ export { buildServiceManifest, detectFrameworks, extractServiceUnits, isServiceProject, type ServiceEndpointUnit, type ServiceFileExtraction, type ServiceManifestV0, type ServiceTaskUnit, } from './serviceDiscovery.js';
2
+ export { buildServiceManifestFromPaths, listCandidateSourceFiles, type ListCandidateSourceFilesOptions, } from './projectScan.js';
3
+ export { NON_RESOURCE_CATEGORIES, RESOURCE_DOMAINS, admitAnalyzeCandidate, isNonResourceCategory, type AdmissionDecision, type NonResourceCategory, type ResourceDomain, } from './resourceAdmission.js';
4
+ export { languageForPath, toResourceFindingCandidate, type ResourceCandidateHit, type ResourceFindingCandidateV1, type ResourcePrimaryDomain, } from './resourceFindingCandidate.js';
5
+ export { detectRetryAmplificationHits } from './retryAmplificationDetector.js';
6
+ export { detectApiOverconsumptionHits } from './apiOverconsumptionDetector.js';
7
+ export { detectUnboundedParallelismHits } from './unboundedParallelismDetector.js';
8
+ export { detectInferenceInsideLoopHits, detectModelLoadInLoopHits, detectPollingIdleHits, detectPerItemInsteadOfBatchHits, detectProcessChurnHits, detectRepeatedWorkHits, } from './cpuResourceDetectors.js';
9
+ export { detectFullDatasetInMemoryHits, detectRepeatedFileReadHits, } from './memoryIoDetectors.js';
10
+ export { detectAllHitsAst, detectHotPathHitsAst, detectApiOverconsumptionHitsAst, detectPollingIdleHitsAst, detectProcessChurnHitsAst, detectRepeatedFileReadHitsAst, detectRepeatedWorkHitsAst, detectRetryAmplificationHitsAst, detectUnboundedParallelismHitsAst, detectFullDatasetInMemoryHitsAst, detectPerItemInsteadOfBatchHitsAst, detectModelLoadInLoopHitsAst, detectInferenceInsideLoopHitsAst, type AstHitBundle, } from './astDetectors.js';
11
+ export { collectPreferAstHits, detectApiOverconsumptionHitsPreferAst, detectPollingIdleHitsPreferAst, detectProcessChurnHitsPreferAst, detectRepeatedFileReadHitsPreferAst, detectRepeatedWorkHitsPreferAst, detectRetryAmplificationHitsPreferAst, detectUnboundedParallelismHitsPreferAst, detectFullDatasetInMemoryHitsPreferAst, detectPerItemInsteadOfBatchHitsPreferAst, detectModelLoadInLoopHitsPreferAst, detectInferenceInsideLoopHitsPreferAst, } from './preferAstDetectors.js';
12
+ export { isTypescriptAnalyzeAvailable, tryLoadTypescript, } from './loadTypescript.js';
13
+ export { allResourceFindingsValid, filterValidResourceFindings, validateResourceFindingCandidate, type ResourceFindingValidation, type ResourceFindingValidationError, } from './validateResourceFinding.js';
14
+ export { buildAnalyzeGateSummary, countGateFindings, findingsExceedFailThreshold, formatAnalyzeGateFailureMessage, resolveAnalyzeGateLimit, type AnalyzeGateLimit, type AnalyzeGateSummaryV0, } from './analyzeGate.js';
15
+ export { buildResourceAnalyzeResultFromPaths, buildResourceFindingsFromSources, type BuildResourceFindingsOptions, type ResourceAnalyzeResultV0, } from './buildResourceFindings.js';
@@ -0,0 +1,15 @@
1
+ export { buildServiceManifest, detectFrameworks, extractServiceUnits, isServiceProject, } from './serviceDiscovery.js';
2
+ export { buildServiceManifestFromPaths, listCandidateSourceFiles, } from './projectScan.js';
3
+ export { NON_RESOURCE_CATEGORIES, RESOURCE_DOMAINS, admitAnalyzeCandidate, isNonResourceCategory, } from './resourceAdmission.js';
4
+ export { languageForPath, toResourceFindingCandidate, } from './resourceFindingCandidate.js';
5
+ export { detectRetryAmplificationHits } from './retryAmplificationDetector.js';
6
+ export { detectApiOverconsumptionHits } from './apiOverconsumptionDetector.js';
7
+ export { detectUnboundedParallelismHits } from './unboundedParallelismDetector.js';
8
+ export { detectInferenceInsideLoopHits, detectModelLoadInLoopHits, detectPollingIdleHits, detectPerItemInsteadOfBatchHits, detectProcessChurnHits, detectRepeatedWorkHits, } from './cpuResourceDetectors.js';
9
+ export { detectFullDatasetInMemoryHits, detectRepeatedFileReadHits, } from './memoryIoDetectors.js';
10
+ export { detectAllHitsAst, detectHotPathHitsAst, detectApiOverconsumptionHitsAst, detectPollingIdleHitsAst, detectProcessChurnHitsAst, detectRepeatedFileReadHitsAst, detectRepeatedWorkHitsAst, detectRetryAmplificationHitsAst, detectUnboundedParallelismHitsAst, detectFullDatasetInMemoryHitsAst, detectPerItemInsteadOfBatchHitsAst, detectModelLoadInLoopHitsAst, detectInferenceInsideLoopHitsAst, } from './astDetectors.js';
11
+ export { collectPreferAstHits, detectApiOverconsumptionHitsPreferAst, detectPollingIdleHitsPreferAst, detectProcessChurnHitsPreferAst, detectRepeatedFileReadHitsPreferAst, detectRepeatedWorkHitsPreferAst, detectRetryAmplificationHitsPreferAst, detectUnboundedParallelismHitsPreferAst, detectFullDatasetInMemoryHitsPreferAst, detectPerItemInsteadOfBatchHitsPreferAst, detectModelLoadInLoopHitsPreferAst, detectInferenceInsideLoopHitsPreferAst, } from './preferAstDetectors.js';
12
+ export { isTypescriptAnalyzeAvailable, tryLoadTypescript, } from './loadTypescript.js';
13
+ export { allResourceFindingsValid, filterValidResourceFindings, validateResourceFindingCandidate, } from './validateResourceFinding.js';
14
+ export { buildAnalyzeGateSummary, countGateFindings, findingsExceedFailThreshold, formatAnalyzeGateFailureMessage, resolveAnalyzeGateLimit, } from './analyzeGate.js';
15
+ export { buildResourceAnalyzeResultFromPaths, buildResourceFindingsFromSources, } from './buildResourceFindings.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Optional TypeScript compiler API loader for Analyze AST detectors.
3
+ * Missing ``typescript`` → callers fall back to heuristics (no hard dep).
4
+ */
5
+ export type TypescriptApi = typeof import('typescript');
6
+ /** Return typescript module or null when unavailable. */
7
+ export declare function tryLoadTypescript(): TypescriptApi | null;
8
+ export declare function isTypescriptAnalyzeAvailable(): boolean;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Optional TypeScript compiler API loader for Analyze AST detectors.
3
+ * Missing ``typescript`` → callers fall back to heuristics (no hard dep).
4
+ */
5
+ import { createRequire } from 'node:module';
6
+ let cached;
7
+ /** Return typescript module or null when unavailable. */
8
+ export function tryLoadTypescript() {
9
+ if (cached !== undefined)
10
+ return cached;
11
+ try {
12
+ const require = createRequire(import.meta.url);
13
+ cached = require('typescript');
14
+ return cached;
15
+ }
16
+ catch {
17
+ cached = null;
18
+ return null;
19
+ }
20
+ }
21
+ export function isTypescriptAnalyzeAvailable() {
22
+ return tryLoadTypescript() !== null;
23
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Heuristic JS/TS twins for memory / storage RD0 candidate_map rules:
3
+ * - repeated_file_reads → repeated_work / storage.io
4
+ * - full_dataset_in_memory → memory_retention_or_materialization / memory.host
5
+ */
6
+ import { type ResourceCandidateHit } from './resourceFindingCandidate.js';
7
+ /**
8
+ * Python ``repeated_file_reads`` twin — fs read inside a loop.
9
+ */
10
+ export declare function detectRepeatedFileReadHits(source: string, relPath: string): ResourceCandidateHit[];
11
+ /**
12
+ * Python ``full_dataset_in_memory`` twin — load+parse entire file payload.
13
+ * Flags JSON.parse(readFile…) on one line, or readFile then JSON.parse within 2 lines.
14
+ */
15
+ export declare function detectFullDatasetInMemoryHits(source: string, relPath: string): ResourceCandidateHit[];