@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,68 @@
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 { FILE_READ_RE, forEachSourceLine, stripLineComment, } from './heuristicScan.js';
7
+ import { languageForPath, } from './resourceFindingCandidate.js';
8
+ const DETECTOR_VERSION = '0.1.0';
9
+ const JSON_PARSE_RE = /\bJSON\.parse\s*\(/;
10
+ /**
11
+ * Python ``repeated_file_reads`` twin — fs read inside a loop.
12
+ */
13
+ export function detectRepeatedFileReadHits(source, relPath) {
14
+ const hits = [];
15
+ const language = languageForPath(relPath);
16
+ const path = relPath.replace(/\\/g, '/');
17
+ forEachSourceLine(source, ({ line, lineNo, inLoop }) => {
18
+ if (!inLoop || !FILE_READ_RE.test(line))
19
+ return;
20
+ hits.push({
21
+ mechanism_id: 'repeated_work',
22
+ primary_domain: 'storage.io',
23
+ admission_domain: 'storage.io',
24
+ title: 'File read inside loop may repeat avoidable I/O work',
25
+ rel_path: path,
26
+ start_line: lineNo,
27
+ end_line: lineNo,
28
+ language,
29
+ detector_id: 'js.static.repeated_file_reads',
30
+ detector_version: DETECTOR_VERSION,
31
+ legacy_issue_type: 'repeated_file_reads',
32
+ });
33
+ });
34
+ return hits;
35
+ }
36
+ /**
37
+ * Python ``full_dataset_in_memory`` twin — load+parse entire file payload.
38
+ * Flags JSON.parse(readFile…) on one line, or readFile then JSON.parse within 2 lines.
39
+ */
40
+ export function detectFullDatasetInMemoryHits(source, relPath) {
41
+ const hits = [];
42
+ const language = languageForPath(relPath);
43
+ const path = relPath.replace(/\\/g, '/');
44
+ const lines = source.split(/\r?\n/).map((l) => stripLineComment(l));
45
+ for (let i = 0; i < lines.length; i += 1) {
46
+ const line = lines[i] ?? '';
47
+ const lineNo = i + 1;
48
+ const sameLine = JSON_PARSE_RE.test(line) && FILE_READ_RE.test(line);
49
+ const window = [lines[i + 1] ?? '', lines[i + 2] ?? ''].join('\n');
50
+ const readThenParse = FILE_READ_RE.test(line) && JSON_PARSE_RE.test(window);
51
+ if (!sameLine && !readThenParse)
52
+ continue;
53
+ hits.push({
54
+ mechanism_id: 'memory_retention_or_materialization',
55
+ primary_domain: 'memory.host',
56
+ admission_domain: 'memory.host',
57
+ title: 'Loading and parsing an entire file may retain a full dataset in memory',
58
+ rel_path: path,
59
+ start_line: lineNo,
60
+ end_line: lineNo,
61
+ language,
62
+ detector_id: 'js.static.full_dataset_in_memory',
63
+ detector_version: DETECTOR_VERSION,
64
+ legacy_issue_type: 'full_dataset_in_memory',
65
+ });
66
+ }
67
+ return hits;
68
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Prefer TypeScript AST for Analyze detectors; fall back to heuristics.
3
+ * Parses each source at most once via ``detectAllHitsAst``.
4
+ */
5
+ import type { ResourceCandidateHit } from './resourceFindingCandidate.js';
6
+ /** Collect all prefer-AST hits for one source file (single parse when TS available). */
7
+ export declare function collectPreferAstHits(source: string, relPath: string): ResourceCandidateHit[];
8
+ export declare function detectRetryAmplificationHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
9
+ export declare function detectRepeatedFileReadHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
10
+ export declare function detectApiOverconsumptionHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
11
+ export declare function detectUnboundedParallelismHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
12
+ export declare function detectPollingIdleHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
13
+ export declare function detectRepeatedWorkHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
14
+ export declare function detectProcessChurnHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
15
+ export declare function detectFullDatasetInMemoryHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
16
+ export declare function detectPerItemInsteadOfBatchHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
17
+ export declare function detectModelLoadInLoopHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
18
+ export declare function detectInferenceInsideLoopHitsPreferAst(source: string, relPath: string): ResourceCandidateHit[];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Prefer TypeScript AST for Analyze detectors; fall back to heuristics.
3
+ * Parses each source at most once via ``detectAllHitsAst``.
4
+ */
5
+ import { detectApiOverconsumptionHits } from './apiOverconsumptionDetector.js';
6
+ import { detectAllHitsAst, } from './astDetectors.js';
7
+ import { detectInferenceInsideLoopHits, detectModelLoadInLoopHits, detectPollingIdleHits, detectPerItemInsteadOfBatchHits, detectProcessChurnHits, detectRepeatedWorkHits, } from './cpuResourceDetectors.js';
8
+ import { detectFullDatasetInMemoryHits, detectRepeatedFileReadHits, } from './memoryIoDetectors.js';
9
+ import { detectRetryAmplificationHits } from './retryAmplificationDetector.js';
10
+ import { detectUnboundedParallelismHits } from './unboundedParallelismDetector.js';
11
+ function prefer(astHits, fallback) {
12
+ if (astHits !== null && astHits !== undefined)
13
+ return astHits;
14
+ return fallback();
15
+ }
16
+ /** Collect all prefer-AST hits for one source file (single parse when TS available). */
17
+ export function collectPreferAstHits(source, relPath) {
18
+ const bundle = detectAllHitsAst(source, relPath);
19
+ if (bundle) {
20
+ return [
21
+ ...bundle.retry,
22
+ ...bundle.overconsumption,
23
+ ...bundle.unbounded,
24
+ ...bundle.polling,
25
+ ...bundle.repeatedWork,
26
+ ...bundle.processChurn,
27
+ ...bundle.modelLoad,
28
+ ...bundle.inference,
29
+ ...bundle.fileReads,
30
+ ...bundle.fullDataset,
31
+ ...bundle.perItem,
32
+ ];
33
+ }
34
+ return [
35
+ ...detectRetryAmplificationHits(source, relPath),
36
+ ...detectApiOverconsumptionHits(source, relPath),
37
+ ...detectUnboundedParallelismHits(source, relPath),
38
+ ...detectPollingIdleHits(source, relPath),
39
+ ...detectRepeatedWorkHits(source, relPath),
40
+ ...detectProcessChurnHits(source, relPath),
41
+ ...detectModelLoadInLoopHits(source, relPath),
42
+ ...detectInferenceInsideLoopHits(source, relPath),
43
+ ...detectRepeatedFileReadHits(source, relPath),
44
+ ...detectFullDatasetInMemoryHits(source, relPath),
45
+ ...detectPerItemInsteadOfBatchHits(source, relPath),
46
+ ];
47
+ }
48
+ export function detectRetryAmplificationHitsPreferAst(source, relPath) {
49
+ const bundle = detectAllHitsAst(source, relPath);
50
+ return prefer(bundle?.retry ?? null, () => detectRetryAmplificationHits(source, relPath));
51
+ }
52
+ export function detectRepeatedFileReadHitsPreferAst(source, relPath) {
53
+ const bundle = detectAllHitsAst(source, relPath);
54
+ return prefer(bundle?.fileReads ?? null, () => detectRepeatedFileReadHits(source, relPath));
55
+ }
56
+ export function detectApiOverconsumptionHitsPreferAst(source, relPath) {
57
+ const bundle = detectAllHitsAst(source, relPath);
58
+ return prefer(bundle?.overconsumption ?? null, () => detectApiOverconsumptionHits(source, relPath));
59
+ }
60
+ export function detectUnboundedParallelismHitsPreferAst(source, relPath) {
61
+ const bundle = detectAllHitsAst(source, relPath);
62
+ return prefer(bundle?.unbounded ?? null, () => detectUnboundedParallelismHits(source, relPath));
63
+ }
64
+ export function detectPollingIdleHitsPreferAst(source, relPath) {
65
+ const bundle = detectAllHitsAst(source, relPath);
66
+ return prefer(bundle?.polling ?? null, () => detectPollingIdleHits(source, relPath));
67
+ }
68
+ export function detectRepeatedWorkHitsPreferAst(source, relPath) {
69
+ const bundle = detectAllHitsAst(source, relPath);
70
+ return prefer(bundle?.repeatedWork ?? null, () => detectRepeatedWorkHits(source, relPath));
71
+ }
72
+ export function detectProcessChurnHitsPreferAst(source, relPath) {
73
+ const bundle = detectAllHitsAst(source, relPath);
74
+ return prefer(bundle?.processChurn ?? null, () => detectProcessChurnHits(source, relPath));
75
+ }
76
+ export function detectFullDatasetInMemoryHitsPreferAst(source, relPath) {
77
+ const bundle = detectAllHitsAst(source, relPath);
78
+ return prefer(bundle?.fullDataset ?? null, () => detectFullDatasetInMemoryHits(source, relPath));
79
+ }
80
+ export function detectPerItemInsteadOfBatchHitsPreferAst(source, relPath) {
81
+ const bundle = detectAllHitsAst(source, relPath);
82
+ return prefer(bundle?.perItem ?? null, () => detectPerItemInsteadOfBatchHits(source, relPath));
83
+ }
84
+ export function detectModelLoadInLoopHitsPreferAst(source, relPath) {
85
+ const bundle = detectAllHitsAst(source, relPath);
86
+ return prefer(bundle?.modelLoad ?? null, () => detectModelLoadInLoopHits(source, relPath));
87
+ }
88
+ export function detectInferenceInsideLoopHitsPreferAst(source, relPath) {
89
+ const bundle = detectAllHitsAst(source, relPath);
90
+ return prefer(bundle?.inference ?? null, () => detectInferenceInsideLoopHits(source, relPath));
91
+ }
@@ -0,0 +1,19 @@
1
+ import { type ServiceManifestV0 } from './serviceDiscovery.js';
2
+ export type ListCandidateSourceFilesOptions = {
3
+ /** Max files to return (default 200). */
4
+ maxFiles?: number;
5
+ /** Max bytes to read per file when building a manifest (default 512 KiB). */
6
+ maxFileBytes?: number;
7
+ extensions?: Iterable<string>;
8
+ skipDirNames?: Iterable<string>;
9
+ };
10
+ /**
11
+ * List repo-relative JS/TS source paths under ``projectDir`` (bounded).
12
+ * Never throws — returns [] on errors.
13
+ */
14
+ export declare function listCandidateSourceFiles(projectDir: string, opts?: ListCandidateSourceFilesOptions): string[];
15
+ /**
16
+ * Read supplied repo-relative paths (or auto-walk) and build ``service_manifest_v0``.
17
+ * Never throws — empty-but-shaped manifest on any error.
18
+ */
19
+ export declare function buildServiceManifestFromPaths(projectDir: string, relPaths?: Iterable<string> | null, opts?: ListCandidateSourceFilesOptions): ServiceManifestV0;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Bounded project walk for MP4 Analyze scaffold.
3
+ * Never touches RunSession upload. Skips node_modules / dist / .git by default.
4
+ */
5
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
6
+ import { join, relative, sep } from 'node:path';
7
+ import { buildServiceManifest } from './serviceDiscovery.js';
8
+ const DEFAULT_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx']);
9
+ const DEFAULT_SKIP_DIR_NAMES = new Set([
10
+ 'node_modules',
11
+ 'dist',
12
+ 'build',
13
+ 'coverage',
14
+ '.git',
15
+ '.next',
16
+ '.turbo',
17
+ '.cache',
18
+ 'out',
19
+ ]);
20
+ function normalizeRel(path) {
21
+ return path.split(sep).join('/');
22
+ }
23
+ /**
24
+ * List repo-relative JS/TS source paths under ``projectDir`` (bounded).
25
+ * Never throws — returns [] on errors.
26
+ */
27
+ export function listCandidateSourceFiles(projectDir, opts = {}) {
28
+ const maxFiles = Math.max(1, opts.maxFiles ?? 200);
29
+ const extensions = new Set([...(opts.extensions ?? DEFAULT_EXTENSIONS)].map((e) => e.toLowerCase()));
30
+ const skip = new Set([...(opts.skipDirNames ?? DEFAULT_SKIP_DIR_NAMES)].map((n) => n.toLowerCase()));
31
+ const out = [];
32
+ const walk = (absDir) => {
33
+ if (out.length >= maxFiles)
34
+ return;
35
+ let entries;
36
+ try {
37
+ entries = readdirSync(absDir, { withFileTypes: true });
38
+ }
39
+ catch {
40
+ return;
41
+ }
42
+ for (const entry of entries) {
43
+ if (out.length >= maxFiles)
44
+ return;
45
+ const name = entry.name;
46
+ if (name.startsWith('.') && name !== '.env.example') {
47
+ // skip hidden dirs/files except harmless examples
48
+ if (entry.isDirectory())
49
+ continue;
50
+ }
51
+ const abs = join(absDir, name);
52
+ if (entry.isDirectory()) {
53
+ if (skip.has(name.toLowerCase()))
54
+ continue;
55
+ walk(abs);
56
+ continue;
57
+ }
58
+ if (!entry.isFile())
59
+ continue;
60
+ const lower = name.toLowerCase();
61
+ const dot = lower.lastIndexOf('.');
62
+ if (dot < 0)
63
+ continue;
64
+ const ext = lower.slice(dot);
65
+ if (!extensions.has(ext))
66
+ continue;
67
+ out.push(normalizeRel(relative(projectDir, abs)));
68
+ }
69
+ };
70
+ try {
71
+ const st = statSync(projectDir);
72
+ if (!st.isDirectory())
73
+ return [];
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ walk(projectDir);
79
+ return out.sort();
80
+ }
81
+ /**
82
+ * Read supplied repo-relative paths (or auto-walk) and build ``service_manifest_v0``.
83
+ * Never throws — empty-but-shaped manifest on any error.
84
+ */
85
+ export function buildServiceManifestFromPaths(projectDir, relPaths, opts = {}) {
86
+ try {
87
+ const maxFileBytes = Math.max(1024, opts.maxFileBytes ?? 512 * 1024);
88
+ const paths = relPaths
89
+ ? [...relPaths].filter(Boolean).map((p) => normalizeRel(String(p)))
90
+ : listCandidateSourceFiles(projectDir, opts);
91
+ const sources = {};
92
+ for (const rel of paths) {
93
+ try {
94
+ const abs = join(projectDir, rel);
95
+ const st = statSync(abs);
96
+ if (!st.isFile() || st.size > maxFileBytes)
97
+ continue;
98
+ sources[rel] = readFileSync(abs, { encoding: 'utf8' });
99
+ }
100
+ catch {
101
+ continue;
102
+ }
103
+ }
104
+ const manifest = buildServiceManifest(sources);
105
+ manifest.notes = [
106
+ ...manifest.notes,
107
+ `scanned_files=${Object.keys(sources).length}`,
108
+ relPaths ? 'paths=explicit' : 'paths=auto_walk',
109
+ ];
110
+ return manifest;
111
+ }
112
+ catch {
113
+ return buildServiceManifest({});
114
+ }
115
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * MP4 default-deny admission stub for Analyze candidates.
3
+ *
4
+ * Used by ``toResourceFindingCandidate`` before emitting ``resource_finding_v1``.
5
+ * Rejects clearly non-resource categories so Node cannot silently copy Python
6
+ * legacy finding types into the resource path. Shared policy / TCK remain
7
+ * authoritative when fully wired.
8
+ */
9
+ /** Categories that must never enter the resource Analyze/Optimize path. */
10
+ export declare const NON_RESOURCE_CATEGORIES: readonly ["hardcoded_secret", "security", "sql_injection", "architecture", "style", "maintainability", "missing_test", "generic_complexity", "generic_reliability", "generic_correctness"];
11
+ export type NonResourceCategory = (typeof NON_RESOURCE_CATEGORIES)[number];
12
+ /** Domains allowed to become resource candidates (scaffold allow-list). */
13
+ export declare const RESOURCE_DOMAINS: readonly ["cpu", "gpu", "memory", "io", "network", "energy", "external_api", "quality_outcome", "compute.cpu", "compute.gpu", "memory.host", "memory.gpu", "storage.io", "network.io"];
14
+ export type ResourceDomain = (typeof RESOURCE_DOMAINS)[number];
15
+ export type AdmissionDecision = {
16
+ admitted: true;
17
+ domain: ResourceDomain;
18
+ reason: 'resource_domain_allowlisted';
19
+ } | {
20
+ admitted: false;
21
+ reason: 'non_resource_category' | 'unknown_or_missing_domain' | 'explicit_deny';
22
+ category?: string;
23
+ detail?: string;
24
+ };
25
+ /**
26
+ * Default-deny: admit only when ``domain`` is a known resource domain and
27
+ * ``category`` (if present) is not a forbidden non-resource class.
28
+ */
29
+ export declare function admitAnalyzeCandidate(input: {
30
+ domain?: string | null;
31
+ category?: string | null;
32
+ }): AdmissionDecision;
33
+ export declare function isNonResourceCategory(category: string): boolean;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * MP4 default-deny admission stub for Analyze candidates.
3
+ *
4
+ * Used by ``toResourceFindingCandidate`` before emitting ``resource_finding_v1``.
5
+ * Rejects clearly non-resource categories so Node cannot silently copy Python
6
+ * legacy finding types into the resource path. Shared policy / TCK remain
7
+ * authoritative when fully wired.
8
+ */
9
+ /** Categories that must never enter the resource Analyze/Optimize path. */
10
+ export const NON_RESOURCE_CATEGORIES = [
11
+ 'hardcoded_secret',
12
+ 'security',
13
+ 'sql_injection',
14
+ 'architecture',
15
+ 'style',
16
+ 'maintainability',
17
+ 'missing_test',
18
+ 'generic_complexity',
19
+ 'generic_reliability',
20
+ 'generic_correctness',
21
+ ];
22
+ /** Domains allowed to become resource candidates (scaffold allow-list). */
23
+ export const RESOURCE_DOMAINS = [
24
+ 'cpu',
25
+ 'gpu',
26
+ 'memory',
27
+ 'io',
28
+ 'network',
29
+ 'energy',
30
+ 'external_api',
31
+ 'quality_outcome',
32
+ // Contract primary_domain aliases (resource_finding_v1)
33
+ 'compute.cpu',
34
+ 'compute.gpu',
35
+ 'memory.host',
36
+ 'memory.gpu',
37
+ 'storage.io',
38
+ 'network.io',
39
+ ];
40
+ /** Map contract primary_domain → short admission key when needed. */
41
+ const DOMAIN_ALIASES = {
42
+ 'compute.cpu': 'cpu',
43
+ 'compute.gpu': 'gpu',
44
+ 'memory.host': 'memory',
45
+ 'memory.gpu': 'memory',
46
+ 'storage.io': 'io',
47
+ 'network.io': 'network',
48
+ };
49
+ const NON_RESOURCE_SET = new Set(NON_RESOURCE_CATEGORIES);
50
+ const RESOURCE_SET = new Set(RESOURCE_DOMAINS);
51
+ function normalizeDomain(domain) {
52
+ const d = domain.trim().toLowerCase();
53
+ return DOMAIN_ALIASES[d] || d;
54
+ }
55
+ /**
56
+ * Default-deny: admit only when ``domain`` is a known resource domain and
57
+ * ``category`` (if present) is not a forbidden non-resource class.
58
+ */
59
+ export function admitAnalyzeCandidate(input) {
60
+ const category = String(input.category || '')
61
+ .trim()
62
+ .toLowerCase();
63
+ if (category && NON_RESOURCE_SET.has(category)) {
64
+ return {
65
+ admitted: false,
66
+ reason: 'non_resource_category',
67
+ category,
68
+ detail: 'legacy/non-resource finding classes are rejected by default-deny',
69
+ };
70
+ }
71
+ const raw = String(input.domain || '')
72
+ .trim()
73
+ .toLowerCase();
74
+ const domain = normalizeDomain(raw);
75
+ if (!domain || (!RESOURCE_SET.has(domain) && !RESOURCE_SET.has(raw))) {
76
+ return {
77
+ admitted: false,
78
+ reason: 'unknown_or_missing_domain',
79
+ detail: 'resource Analyze requires an allowlisted primary domain',
80
+ };
81
+ }
82
+ const admittedDomain = (RESOURCE_SET.has(raw) ? raw : domain);
83
+ return {
84
+ admitted: true,
85
+ domain: admittedDomain,
86
+ reason: 'resource_domain_allowlisted',
87
+ };
88
+ }
89
+ export function isNonResourceCategory(category) {
90
+ return NON_RESOURCE_SET.has(String(category || '').trim().toLowerCase());
91
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * MP4 Node twin of Python resource-candidate IR (``resource_finding_v1``).
3
+ *
4
+ * Emits **candidates** only (evidence_state=candidate). Does not upload, does not
5
+ * change RunSession, and does not claim Optimize / verified savings.
6
+ */
7
+ /** Contract primary_domain values used by Node Analyze candidates. */
8
+ export type ResourcePrimaryDomain = 'compute.cpu' | 'compute.gpu' | 'memory.host' | 'memory.gpu' | 'storage.io' | 'network.io' | 'external_api';
9
+ export type ResourceFindingCandidateV1 = {
10
+ schema_version: 'resource_finding_v1';
11
+ finding_id: string;
12
+ project_id: string;
13
+ evidence_state: 'candidate';
14
+ comparison_origin: 'none';
15
+ lifecycle_status: 'pending_admission';
16
+ verification_outcome: 'not_run';
17
+ primary_domain: ResourcePrimaryDomain;
18
+ mechanism_id: string;
19
+ title: string;
20
+ scope: {
21
+ kind: 'function' | 'service_unit' | 'task' | 'script';
22
+ id: string;
23
+ language: 'javascript' | 'typescript';
24
+ runtime: 'nodejs';
25
+ source_locations: Array<{
26
+ path: string;
27
+ start_line: number;
28
+ end_line: number;
29
+ symbol?: string;
30
+ }>;
31
+ };
32
+ lineage: {
33
+ detector_id: string;
34
+ detector_version: string;
35
+ policy_version: string;
36
+ observation_ids: string[];
37
+ benchmark_result_ids: string[];
38
+ };
39
+ };
40
+ export type ResourceCandidateHit = {
41
+ mechanism_id: string;
42
+ primary_domain: ResourcePrimaryDomain;
43
+ /** Short domain key for local admission stub (e.g. external_api). */
44
+ admission_domain: string;
45
+ title: string;
46
+ rel_path: string;
47
+ start_line: number;
48
+ end_line: number;
49
+ symbol?: string;
50
+ language: 'javascript' | 'typescript';
51
+ detector_id: string;
52
+ detector_version: string;
53
+ legacy_issue_type?: string;
54
+ /** Optional reject category — when set, must pass default-deny. */
55
+ category?: string;
56
+ };
57
+ /**
58
+ * Shape a detector hit into ``resource_finding_v1`` candidate, or null if denied.
59
+ */
60
+ export declare function toResourceFindingCandidate(hit: ResourceCandidateHit, projectId?: string): ResourceFindingCandidateV1 | null;
61
+ export declare function languageForPath(relPath: string): 'javascript' | 'typescript';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * MP4 Node twin of Python resource-candidate IR (``resource_finding_v1``).
3
+ *
4
+ * Emits **candidates** only (evidence_state=candidate). Does not upload, does not
5
+ * change RunSession, and does not claim Optimize / verified savings.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { admitAnalyzeCandidate } from './resourceAdmission.js';
9
+ const POLICY_VERSION = '0.1.0';
10
+ function languageFromPath(relPath) {
11
+ const lower = relPath.toLowerCase();
12
+ if (lower.endsWith('.ts') || lower.endsWith('.tsx'))
13
+ return 'typescript';
14
+ return 'javascript';
15
+ }
16
+ function stableFindingId(hit, projectId) {
17
+ const preimage = [
18
+ projectId,
19
+ hit.mechanism_id,
20
+ hit.rel_path,
21
+ String(hit.start_line),
22
+ hit.detector_id,
23
+ ].join('|');
24
+ const digest = createHash('sha256').update(preimage).digest('hex').slice(0, 12);
25
+ return `rf_${hit.mechanism_id.slice(0, 24)}_${digest}`;
26
+ }
27
+ /**
28
+ * Shape a detector hit into ``resource_finding_v1`` candidate, or null if denied.
29
+ */
30
+ export function toResourceFindingCandidate(hit, projectId = 'local') {
31
+ const decision = admitAnalyzeCandidate({
32
+ domain: hit.admission_domain,
33
+ category: hit.category,
34
+ });
35
+ if (!decision.admitted)
36
+ return null;
37
+ const language = hit.language || languageFromPath(hit.rel_path);
38
+ const scopeId = (hit.symbol || hit.rel_path).slice(0, 200);
39
+ return {
40
+ schema_version: 'resource_finding_v1',
41
+ finding_id: stableFindingId(hit, projectId).slice(0, 128),
42
+ project_id: String(projectId || 'local').slice(0, 64),
43
+ evidence_state: 'candidate',
44
+ comparison_origin: 'none',
45
+ lifecycle_status: 'pending_admission',
46
+ verification_outcome: 'not_run',
47
+ primary_domain: hit.primary_domain,
48
+ mechanism_id: hit.mechanism_id,
49
+ title: hit.title.slice(0, 200),
50
+ scope: {
51
+ kind: hit.symbol ? 'function' : 'script',
52
+ id: scopeId,
53
+ language,
54
+ runtime: 'nodejs',
55
+ source_locations: [
56
+ {
57
+ path: hit.rel_path.replace(/\\/g, '/').slice(0, 512),
58
+ start_line: Math.max(1, hit.start_line),
59
+ end_line: Math.max(hit.start_line, hit.end_line),
60
+ ...(hit.symbol ? { symbol: hit.symbol.slice(0, 120) } : {}),
61
+ },
62
+ ],
63
+ },
64
+ lineage: {
65
+ detector_id: hit.detector_id,
66
+ detector_version: hit.detector_version,
67
+ policy_version: POLICY_VERSION,
68
+ observation_ids: [],
69
+ benchmark_result_ids: [],
70
+ },
71
+ };
72
+ }
73
+ export function languageForPath(relPath) {
74
+ return languageFromPath(relPath);
75
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Heuristic JS/TS twin of Python ``api_usage_audit.api_call_in_retry_loop``.
3
+ *
4
+ * Maps via RD0 disposition → mechanism ``retry_amplification`` / domain ``external_api``.
5
+ * Not a TypeScript AST yet — brace-depth + line heuristics only.
6
+ */
7
+ import { type ResourceCandidateHit } from './resourceFindingCandidate.js';
8
+ /**
9
+ * Detect API calls nested in retry-ish loops in one source file.
10
+ */
11
+ export declare function detectRetryAmplificationHits(source: string, relPath: string): ResourceCandidateHit[];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Heuristic JS/TS twin of Python ``api_usage_audit.api_call_in_retry_loop``.
3
+ *
4
+ * Maps via RD0 disposition → mechanism ``retry_amplification`` / domain ``external_api``.
5
+ * Not a TypeScript AST yet — brace-depth + line heuristics only.
6
+ */
7
+ import { advanceBraceDepth, hasApiCall, RETRY_HEADER_RE, stripLineComment, } from './heuristicScan.js';
8
+ import { languageForPath, } from './resourceFindingCandidate.js';
9
+ const DETECTOR_ID = 'js.static.retry_loop';
10
+ const DETECTOR_VERSION = '0.1.0';
11
+ const LEGACY_ISSUE = 'api_call_in_retry_loop';
12
+ const MECHANISM = 'retry_amplification';
13
+ const TITLE = 'Retry loop may amplify billed API attempts';
14
+ /**
15
+ * Detect API calls nested in retry-ish loops in one source file.
16
+ */
17
+ export function detectRetryAmplificationHits(source, relPath) {
18
+ const lines = source.split(/\r?\n/);
19
+ const hits = [];
20
+ let braceDepth = 0;
21
+ /** Brace depth of the retry loop header line (body is deeper). */
22
+ let retryAtDepth = null;
23
+ const language = languageForPath(relPath);
24
+ const path = relPath.replace(/\\/g, '/');
25
+ for (let i = 0; i < lines.length; i += 1) {
26
+ const line = stripLineComment(lines[i] ?? '');
27
+ const lineNo = i + 1;
28
+ const isRetryHeader = RETRY_HEADER_RE.test(line);
29
+ const before = braceDepth;
30
+ const { braceDepth: next, openedBrace } = advanceBraceDepth(line, braceDepth);
31
+ braceDepth = next;
32
+ if (isRetryHeader && retryAtDepth === null) {
33
+ retryAtDepth = openedBrace ? before : braceDepth;
34
+ }
35
+ if (retryAtDepth !== null && braceDepth <= retryAtDepth) {
36
+ retryAtDepth = null;
37
+ }
38
+ if (retryAtDepth !== null && braceDepth > retryAtDepth && hasApiCall(line)) {
39
+ hits.push({
40
+ mechanism_id: MECHANISM,
41
+ primary_domain: 'external_api',
42
+ admission_domain: 'external_api',
43
+ title: TITLE,
44
+ rel_path: path,
45
+ start_line: lineNo,
46
+ end_line: lineNo,
47
+ language,
48
+ detector_id: DETECTOR_ID,
49
+ detector_version: DETECTOR_VERSION,
50
+ legacy_issue_type: LEGACY_ISSUE,
51
+ });
52
+ }
53
+ }
54
+ return hits;
55
+ }