@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog — @netgreener/runtime
2
+
3
+ ## [0.1.0] — 2026-09-24
4
+
5
+ ### Added
6
+ - Express / Fastify / Nest-via-adapter middleware with tenant attribution
7
+ - Outbound `fetch` metering (`external_api_v0`), optional axios/undici wraps
8
+ - BullMQ processor wrap and process/cron helpers
9
+ - Default `NETGREENER_EXPORT_MODE=direct` upload to NetGreener API
10
+ - Opt-in collector IPC and thin-flush documentation in the tutorial
11
+ - Local `netgreener analyze` scaffold (`run` / `optimize` stubs)
12
+
13
+ ### Notes
14
+ - First public npm release ships with npm provenance from this repository
15
+ - Process CPU/RSS are measured on instrumented hooks; cgroup/process-tree
16
+ sampling is not claimed in this version
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @netgreener/runtime
2
+
3
+ NetGreener Node.js Runtime — thin adapter that meters Express / Fastify / Nest /
4
+ BullMQ / process workloads and uploads RunSessions to the NetGreener API.
5
+
6
+ ## Docs
7
+
8
+ - **Tutorial:** [TUTORIAL.md](./TUTORIAL.md)
9
+ - **Changelog:** [CHANGELOG.md](./CHANGELOG.md)
10
+
11
+ ## Install (when published)
12
+
13
+ ```bash
14
+ npm install @netgreener/runtime
15
+ ```
16
+
17
+ ## Develop
18
+
19
+ ```bash
20
+ npm ci
21
+ npm test
22
+ ```
23
+
24
+ ## License
25
+
26
+ See package `license` field. Source of truth for internal engineering remains
27
+ on the private Azure DevOps repository; this GitHub repo is the public publish
28
+ mirror used for npm provenance.
package/TUTORIAL.md ADDED
@@ -0,0 +1,495 @@
1
+ # Node service runtime tutorial — HTTP, workers, and scripts
2
+
3
+ **Twin of:** [`SERVICE_RUNTIME_TUTORIAL.md`](../docs/SERVICE_RUNTIME_TUTORIAL.md)
4
+ **§4–§6** (FastAPI / Celery / process wrap).
5
+ **Package:** `@netgreener/runtime` (private, experimental).
6
+ **Not claimed:** MP3 production support, npm publish, default collector cutover,
7
+ managed Node sidecar collector parity with Python Celery, or live acceptance
8
+ without your retained evidence.
9
+
10
+ | Need deeper reference | Doc |
11
+ |-----------------------|-----|
12
+ | Node backlog / gates | [`NODE_RUNTIME_PARITY_PLAN.md`](../docs/NODE_RUNTIME_PARITY_PLAN.md) |
13
+ | Worker staging gate | [`WORKER_STAGING_GATE.md`](WORKER_STAGING_GATE.md) |
14
+ | Package status / CI | [`README.md`](README.md), [`ROADMAP.md`](ROADMAP.md) |
15
+ | Tenant dogfood | [`NODE_TENANT_DOGFOOD.md`](../docs/NODE_TENANT_DOGFOOD.md) |
16
+ | Service tokens | [`CI_SERVICE_TOKEN_ONBOARDING.md`](../docs/CI_SERVICE_TOKEN_ONBOARDING.md) |
17
+ | Python twin | [`SERVICE_RUNTIME_TUTORIAL.md`](../docs/SERVICE_RUNTIME_TUTORIAL.md) |
18
+
19
+ ---
20
+
21
+ ## 0. What you will run
22
+
23
+ | Project has… | Situation | NetGreener path | Code change? |
24
+ |--------------|-----------|-----------------|--------------|
25
+ | **Express** HTTP API | Per-**route** KPIs | `netgreenerExpressMiddleware` | **Yes — one middleware line** (§4) |
26
+ | **Fastify** HTTP API | Per-**route** KPIs | `netgreenerFastifyPlugin` | **Yes — one `register`** (§4) |
27
+ | **Nest** | Express or Fastify under the hood | `applyNetGreenerNestHooks(app)` | **Yes — one hook** (§4) |
28
+ | **BullMQ** worker | Per-**task** KPIs | `netgreenerBullMqProcessor` | **Yes — wrap the processor** (§5) |
29
+ | **Script / cron / batch** `node …` | Process window + outbound `fetch` | `startProcessRuntime` / `runWithProcessTenant` | **Yes — start helper** (§6) |
30
+
31
+ Default upload mode remains **`NETGREENER_EXPORT_MODE=direct`**. Sibling collector
32
+ IPC is opt-in only; do not cut over the default without explicit authorization.
33
+
34
+ ```text
35
+ HTTP: middleware/plugin → window → POST /api/v1/runsessions/ (default direct; or dry-run)
36
+ BullMQ: wrapped processor → window → same upload path (v0; no Python-style managed sidecar yet)
37
+ Process: startProcessRuntime → window → same upload path
38
+
39
+ Opt-in: NETGREENER_EXPORT_MODE=collector → length-prefixed IPC → sibling collector spool
40
+ (default remains direct; see §4.5)
41
+ ```
42
+
43
+ ---
44
+
45
+ ## 1. Worksheet (your project)
46
+
47
+ ```text
48
+ App entry file: ________ (creates Express app or Fastify instance)
49
+ Framework: Express / Fastify / Nest→Express / Nest→Fastify
50
+ ORG_ID: ________
51
+ PROJECT_ID: ________
52
+ API URL: https://core-api.netgreener.com (or your host)
53
+ Token secret name: NETGREENER_TOKEN
54
+ Dry-run first?: yes (recommended)
55
+ ```
56
+
57
+ Find **ORG_ID** / **PROJECT_ID** in the NetGreener dashboard (project URL looks like
58
+ `https://dashboard.netgreener.com/projects/<PROJECT_ID>`).
59
+
60
+ ---
61
+
62
+ ## 2. Get a token
63
+
64
+ **Automation / servers (recommended):**
65
+
66
+ 1. Create an API / service token (`ngs_…`) in the dashboard.
67
+ 2. Store it as secret `NETGREENER_TOKEN`.
68
+ 3. Details: [`CI_SERVICE_TOKEN_ONBOARDING.md`](../docs/CI_SERVICE_TOKEN_ONBOARDING.md).
69
+
70
+ **Local dry-run only:** you can skip the token and set `NETGREENER_RUNTIME_DRY_RUN=1`
71
+ (see §4.4). Dry-run builds a real payload shape and does **not** upload.
72
+
73
+ ---
74
+
75
+ ## 3. Install `@netgreener/runtime` in that project
76
+
77
+ The package is **private** today (Azure DevOps / internal registry). Install from your
78
+ approved feed or path — there is **no** public npm publish yet.
79
+
80
+ ```bash
81
+ # Example once the private feed is configured for your project:
82
+ npm install @netgreener/runtime
83
+ ```
84
+
85
+ Engines: Node **22** or **24** (see `package.json` `engines`).
86
+
87
+ Verify the import resolves:
88
+
89
+ ```js
90
+ import { netgreenerExpressMiddleware } from '@netgreener/runtime'
91
+ console.log(typeof netgreenerExpressMiddleware)
92
+ ```
93
+
94
+ ---
95
+
96
+ ## 4. Express or Fastify — any Node HTTP API
97
+
98
+ Starting the app with `node`, `tsx`, PM2, or a container entrypoint does not change
99
+ the hook — those are process managers/servers.
100
+
101
+ ### 4.1 Express — one middleware line
102
+
103
+ In the **same file** that creates `const app = express()` (not only in routers):
104
+
105
+ ```js
106
+ import express from 'express'
107
+ import { netgreenerExpressMiddleware, getExpressRuntime } from '@netgreener/runtime'
108
+
109
+ const app = express()
110
+ app.use(netgreenerExpressMiddleware())
111
+
112
+ app.get('/health', (_req, res) => res.send('ok'))
113
+ // ...
114
+ ```
115
+
116
+ Optional manual flush (tests / graceful shutdown):
117
+
118
+ ```js
119
+ await getExpressRuntime().flush('manual')
120
+ ```
121
+
122
+ ### 4.2 Fastify — one plugin register
123
+
124
+ ```js
125
+ import Fastify from 'fastify'
126
+ import {
127
+ netgreenerFastifyPlugin,
128
+ getFastifyRuntime,
129
+ bindFastifyTenant,
130
+ } from '@netgreener/runtime'
131
+
132
+ const app = Fastify()
133
+ await app.register(netgreenerFastifyPlugin())
134
+
135
+ app.get('/health', async () => 'ok')
136
+
137
+ app.get('/me', async (request) => {
138
+ // After auth: prefer bindFastifyTenant (request-scoped store).
139
+ bindFastifyTenant(request, {
140
+ tenantId: 'org_acme',
141
+ tenantSource: 'verified-auth',
142
+ })
143
+ return { ok: true }
144
+ })
145
+ ```
146
+
147
+ Notes:
148
+
149
+ - The plugin breaks Fastify encapsulation (`skip-override`) so hooks apply to routes
150
+ registered on the same instance after `register`.
151
+ - Prefixed routes appear as `/prefix/path/:id` in `service_unit` (Fastify template).
152
+ - Raw request URLs are never metering fallbacks (`<unmatched>` when no template).
153
+
154
+ ### 4.2b Nest — via underlying Express or Fastify adapter
155
+
156
+ No separate Nest interception layer. After `NestFactory.create`:
157
+
158
+ ```js
159
+ import { NestFactory } from '@nestjs/core'
160
+ import { applyNetGreenerNestHooks } from '@netgreener/runtime'
161
+ import { AppModule } from './app.module.js'
162
+
163
+ const app = await NestFactory.create(AppModule)
164
+ await applyNetGreenerNestHooks(app)
165
+ await app.listen(3000)
166
+ ```
167
+
168
+ Or explicitly:
169
+
170
+ ```js
171
+ import { netgreenerNestExpressMiddleware } from '@netgreener/runtime'
172
+ // Express adapter:
173
+ app.use(netgreenerNestExpressMiddleware())
174
+
175
+ // Fastify adapter:
176
+ // await app.getHttpAdapter().getInstance().register(netgreenerNestFastifyPlugin())
177
+ ```
178
+
179
+ ### 4.3 Env for the API process
180
+
181
+ **Dry-run (no upload):**
182
+
183
+ ```bash
184
+ NETGREENER_SERVICE_RUNTIME=1
185
+ NETGREENER_RUNTIME_DRY_RUN=1
186
+ NETGREENER_TOKEN=ngs_placeholder
187
+ NETGREENER_PROJECT_ID=1
188
+ # optional tenant header binding:
189
+ # NETGREENER_TENANT_SOURCE=header
190
+ # NETGREENER_TENANT_HEADER=X-Organization-Id
191
+ ```
192
+
193
+ **Live upload (only with a real token; you retain evidence):**
194
+
195
+ ```bash
196
+ NETGREENER_SERVICE_RUNTIME=1
197
+ NETGREENER_API_URL=https://core-api.netgreener.com
198
+ NETGREENER_TOKEN=<token>
199
+ NETGREENER_ORG_ID=<org>
200
+ NETGREENER_PROJECT_ID=<project>
201
+ NETGREENER_DEPLOY_ENVIRONMENT=staging
202
+ # Leave NETGREENER_RUNTIME_DRY_RUN unset or 0
203
+ # Leave NETGREENER_EXPORT_MODE unset (default direct) unless authorized for collector
204
+ ```
205
+
206
+ ### 4.4 Prove it
207
+
208
+ **A. Dry-run / CI smoke (no api_server):**
209
+
210
+ ```bash
211
+ cd path/to/netgreener_node # or your app with the package linked
212
+ npm test
213
+ NETGREENER_RUNTIME_DRY_RUN=1 npm run example:dogfood-tenant
214
+ npm run example:dry-run
215
+ ```
216
+
217
+ Pass = scripts exit 0; payloads validate; no live upload required.
218
+
219
+ **B. Live (authorized):**
220
+
221
+ 1. Deploy / start the API with live env (§4.3).
222
+ 2. Call 2–3 real routes (health + one business route).
223
+ 3. Stop the process (or wait for `NETGREENER_RUNTIME_FLUSH_MINUTES`, default 5).
224
+ 4. Dashboard → **Project `<PROJECT_ID>`** → newest **Run** → **Runtime Ops** / API Runtime.
225
+ 5. Pass = bounded route templates listed (e.g. `GET /health`), not raw customer paths.
226
+ 6. Keep a sanitized evidence record (revision, env, run id) — see
227
+ [`NODE_TENANT_DOGFOOD.md`](../docs/NODE_TENANT_DOGFOOD.md).
228
+
229
+ Live failure (non-2xx / missing token) must **fail** the run; local payload capture is
230
+ not a substitute for a successful upload.
231
+
232
+ ### 4.5 Opt-in sibling collector (HTTP → MP2 IPC)
233
+
234
+ Default remains **`NETGREENER_EXPORT_MODE=direct`** (adapter POSTs RunSession). To send
235
+ HTTP window flushes to a sibling collector instead:
236
+
237
+ 1. Start the collector (see
238
+ [`COLLECTOR_SIBLING.md`](../netgreener_contracts/planning/mp2-collector/COLLECTOR_SIBLING.md)).
239
+ 2. In the **API process** (same middleware as §4.1 / §4.2):
240
+
241
+ ```bash
242
+ NETGREENER_EXPORT_MODE=collector
243
+ NETGREENER_COLLECTOR_ENDPOINT=tcp://127.0.0.1:17999
244
+ # Leave NETGREENER_RUNTIME_DRY_RUN unset — dry-run skips collector IPC on purpose
245
+ ```
246
+
247
+ 3. Hit routes and flush as in §4.4. Successful path = durable spool ACK over length-prefixed
248
+ IPC (`run_session_window`), not a silent fall-back to direct upload.
249
+
250
+ **Honesty:** this proves the Node HTTP → exporter → collector wire (Express, Fastify,
251
+ and Nest-via-Express / Nest-via-Fastify adapters). It does **not** flip the default,
252
+ claim MP2 exit, Observation cloud ingest, or Python-style managed sidecar.
253
+ Worker/process twins: §5.5 / §6.1. Local proofs:
254
+ `express.collector.integration.test.ts`,
255
+ `fastify.collector.integration.test.ts`,
256
+ `nest.collector.integration.test.ts`,
257
+ `nest.fastify.collector.integration.test.ts`.
258
+
259
+ ---
260
+
261
+ ## 5. BullMQ — any worker project (Part 5 twin)
262
+
263
+ Unlike Python Celery (entrypoint wrap, often **no** task code changes), Node BullMQ
264
+ v0 meters by **wrapping the processor** you already pass to `Worker`. There is **no**
265
+ `netgreener runtime entrypoint` CLI for Node yet, and **no** `NETGREENER_COLLECTOR_MODE=managed`
266
+ sidecar requirement for this path — default remains direct flush from the worker process.
267
+
268
+ ### 5.1 Wrap the processor (required for task KPIs)
269
+
270
+ ```js
271
+ import { Worker } from 'bullmq'
272
+ import { netgreenerBullMqProcessor, getBullMqRuntime } from '@netgreener/runtime'
273
+
274
+ const processor = netgreenerBullMqProcessor(async (job) => {
275
+ // your existing job logic
276
+ await fetch('https://api.openai.com/v1/chat/completions', { /* … */ })
277
+ return { ok: true }
278
+ })
279
+
280
+ const worker = new Worker('queue-name', processor, { connection: { /* redis */ } })
281
+
282
+ // optional: await getBullMqRuntime().flush('manual')
283
+ ```
284
+
285
+ Each job records a unit like `task:<job.name>`. Outbound `fetch` / axios made inside
286
+ the processor inherit that task unit when ALS is active.
287
+
288
+ ### 5.2 Tenant from job data
289
+
290
+ Set env (or programmatic config) so tenant is read from `job.data`:
291
+
292
+ ```bash
293
+ NETGREENER_TENANT_SOURCE=task_kwarg
294
+ NETGREENER_TENANT_TASK_KWARG=organization_id # default
295
+ ```
296
+
297
+ Enqueue with the kwarg present:
298
+
299
+ ```js
300
+ await queue.add('ocr', { organization_id: 'org_acme', /* … */ })
301
+ ```
302
+
303
+ ### 5.3 Env for the worker process
304
+
305
+ Same token/project vars as §4.3. Prefer dry-run first:
306
+
307
+ ```bash
308
+ NETGREENER_SERVICE_RUNTIME=1
309
+ NETGREENER_RUNTIME_DRY_RUN=1
310
+ NETGREENER_TENANT_SOURCE=task_kwarg
311
+ ```
312
+
313
+ ### 5.4 Prove it
314
+
315
+ 1. Start the worker with the wrapped processor.
316
+ 2. Enqueue **one** job the way your project already does.
317
+ 3. Flush (shutdown or `getBullMqRuntime().flush('manual')`).
318
+ 4. Dry-run: payload includes `task:<name>` and optional `by_tenant`.
319
+ 5. Live (authorized): dashboard Runtime Ops shows the task row.
320
+
321
+ **Honesty gap vs Python Part 5:** managed collector / process-tree sidecar for Node
322
+ workers is still open under MP2–MP3. Real Redis + `bullmq` Worker smoke exists
323
+ (`examples/bullmq-live-smoke.mjs`; live run **39574**) — that is not managed-sidecar
324
+ parity.
325
+
326
+ ### 5.5 Opt-in sibling collector (BullMQ → MP2 IPC)
327
+
328
+ Same env as §4.5 on the **worker process** (default remains `direct`):
329
+
330
+ ```bash
331
+ NETGREENER_EXPORT_MODE=collector
332
+ NETGREENER_COLLECTOR_ENDPOINT=tcp://127.0.0.1:17999
333
+ # Leave NETGREENER_RUNTIME_DRY_RUN unset — dry-run skips collector IPC
334
+ ```
335
+
336
+ Successful flush = `run_session_window` with `collector: bullmq_worker` and
337
+ `task:<job.name>` units over IPC — **not** a Python-style managed sidecar and **not**
338
+ a silent fall-back to direct upload. Local proof:
339
+ `src/tests/workers.collector.integration.test.ts`.
340
+
341
+ ---
342
+
343
+ ## 6. Scripts, cron, sidecars, batch jobs (Part 6 twin)
344
+
345
+ Use this when the work is **not** an HTTP API and **not** a BullMQ worker:
346
+ nightly jobs, ETL, report generators, simple long-lived Node processes.
347
+
348
+ ```js
349
+ import {
350
+ startProcessRuntime,
351
+ runWithProcessTenant,
352
+ flushProcessRuntime,
353
+ } from '@netgreener/runtime'
354
+
355
+ startProcessRuntime()
356
+
357
+ await runWithProcessTenant('org_acme', async () => {
358
+ // batch work + outbound fetch…
359
+ }, { serviceUnit: 'task:nightly_report' })
360
+
361
+ await flushProcessRuntime('manual')
362
+ ```
363
+
364
+ Env: same as §4.3 (`NETGREENER_SERVICE_RUNTIME=1`, dry-run or live token). Leave
365
+ export mode at default `direct` unless authorized for collector.
366
+
367
+ You get a **process/task** window and supported outbound HTTP observation — **not**
368
+ Express/Fastify per-route rows and **not** BullMQ `task:…` rows unless you also use §5.
369
+
370
+ There is no `netgreener runtime process -- node job.js` CLI wrapper in this package
371
+ yet; call `startProcessRuntime` from your entry file (or a thin launcher you own).
372
+
373
+ ### 6.1 Opt-in sibling collector (process → MP2 IPC)
374
+
375
+ Same env as §4.5 / §5.5. Flush via `flushProcessRuntime('manual')` or shutdown.
376
+ IPC payload uses `collector: node_process`. Still **not** a managed process-tree
377
+ sampler or CLI launcher — those remain MP3. Proof lives in
378
+ `src/tests/workers.collector.integration.test.ts`.
379
+
380
+ ---
381
+
382
+ ## 7. Common failures
383
+
384
+ | Symptom | Check |
385
+ |---------|--------|
386
+ | No rows in dashboard | `NETGREENER_SERVICE_RUNTIME=1`, real token, dry-run unset for live |
387
+ | Only dry-run locally | Expected when `NETGREENER_RUNTIME_DRY_RUN=1` |
388
+ | Raw paths / IDs in units | Bug — should be templates or `<unmatched>`; file an issue |
389
+ | Fastify routes not metered | Ensure `register(netgreenerFastifyPlugin())` before routes; package includes skip-override |
390
+ | Tenant missing after auth (Fastify) | Use `bindFastifyTenant(request, …)` |
391
+ | BullMQ jobs invisible | Processor must be wrapped with `netgreenerBullMqProcessor` |
392
+ | BullMQ tenant missing | `NETGREENER_TENANT_SOURCE=task_kwarg` + kwarg in `job.data` |
393
+ | Package not found on npm | Expected — package is private until MP3 publish |
394
+ | Serverless / Lambda freezes mid-window | Use thin-flush (§9); flush at invoke end; durability is best-effort |
395
+
396
+ ---
397
+
398
+ ## 8. Minimal copy-paste canaries
399
+
400
+ **Express dry-run:** middleware + `/health` + `NETGREENER_RUNTIME_DRY_RUN=1` +
401
+ `getExpressRuntime().flush('manual')`.
402
+
403
+ **Fastify dry-run:** `register(plugin)` + `/health` + dry-run env +
404
+ `getFastifyRuntime().flush('manual')`.
405
+
406
+ **BullMQ dry-run:** wrap processor + one fake job + `getBullMqRuntime().flush('manual')`
407
+ (see `examples/tenant-dogfood.mjs`).
408
+
409
+ **Process dry-run:** `startProcessRuntime` + `runWithProcessTenant` + flush.
410
+
411
+ ---
412
+
413
+ ## 9. Thin-flush / serverless mode
414
+
415
+ Long-running VM / container / K8s Node should keep default **`direct`** or opt-in
416
+ **`collector`** (§4.5). Serverless hosts often cannot run a durable sibling collector.
417
+
418
+ ### When to use thin
419
+
420
+ Set **one** of:
421
+
422
+ ```bash
423
+ NETGREENER_EXPORT_MODE=thin
424
+ # or alias when EXPORT_MODE is unset:
425
+ NETGREENER_FLUSH_MODE=thin
426
+ ```
427
+
428
+ Optional shorter window:
429
+
430
+ ```bash
431
+ NETGREENER_RUNTIME_FLUSH_MINUTES=1
432
+ ```
433
+
434
+ ### What thin does (honesty)
435
+
436
+ | Behavior | Thin mode |
437
+ |----------|-----------|
438
+ | Upload path | Same direct RunSession POST as `direct` today |
439
+ | Durability grade | `best_effort_bounded` (visible; not silent drop) |
440
+ | Sibling collector spool | **No** |
441
+ | Auto-detect platforms | **No** — `detectServerlessPlatformSignals()` is informational only |
442
+
443
+ Platform signals the helper recognizes (for your own wiring / checks):
444
+ `AWS_LAMBDA_FUNCTION_NAME`, `FUNCTIONS_WORKER_RUNTIME` / `AZURE_FUNCTIONS_ENVIRONMENT`,
445
+ `FUNCTION_TARGET` / `FUNCTION_SIGNATURE_TYPE`, `VERCEL` / `VERCEL_ENV`, `NETLIFY`.
446
+
447
+ ```js
448
+ import {
449
+ detectServerlessPlatformSignals,
450
+ exportModeFromEnv,
451
+ } from '@netgreener/runtime'
452
+
453
+ const hint = detectServerlessPlatformSignals()
454
+ // hint.suggestedExportMode === 'thin' when a host signal is present — still not applied.
455
+ ```
456
+
457
+ ### Invoke-end flush (required for thin)
458
+
459
+ Call flush before the isolate freezes:
460
+
461
+ ```js
462
+ import { getExpressRuntime } from '@netgreener/runtime'
463
+
464
+ export async function handler(event, context) {
465
+ try {
466
+ // … handle request with middleware-wrapped app …
467
+ } finally {
468
+ await getExpressRuntime().flush('shutdown')
469
+ }
470
+ }
471
+ ```
472
+
473
+ **Not claimed:** Lambda extensions, automatic freeze hooks, MP3 serverless support, or
474
+ flipping the default off `direct` for long-running Node.
475
+
476
+ ---
477
+
478
+ ## 10. Stop here (honesty)
479
+
480
+ This tutorial does **not** by itself:
481
+ - publish `@netgreener/runtime` to npm
482
+ - establish MP3 support, Python-equivalent managed worker collectors, npm release, or
483
+ live acceptance without your retained evidence
484
+ - flip `NETGREENER_EXPORT_MODE` default away from `direct`
485
+
486
+ Package examples: `examples/express-dry-run.mjs`, `examples/tenant-dogfood.mjs`.
487
+
488
+ ---
489
+
490
+ ## Revision note
491
+
492
+ This tutorial documents **experimental** Node HTTP (N1), Nest-via-adapter, BullMQ,
493
+ and process helpers (N3) as of the stacked Node tutorial slices. It does **not**
494
+ establish MP3 support, Python-equivalent managed worker collectors, npm release, or
495
+ default export-mode cutover.
@@ -0,0 +1,43 @@
1
+ import type { ServiceRuntimeV0, TenantId } from './types.js';
2
+ export declare class ServiceRuntimeAggregator {
3
+ private units;
4
+ private byTenant;
5
+ private windowStartedAt;
6
+ resetWindow(): void;
7
+ windowStartedMs(): number;
8
+ record(sample: {
9
+ serviceUnit: string;
10
+ unitType?: string;
11
+ durationMs: number;
12
+ error?: boolean;
13
+ tenantId?: TenantId | null;
14
+ /** Measured process CPU ms for this sample (CAP-R4); omit → duration proxy. */
15
+ cpuTimeMs?: number;
16
+ /** Measured RSS KiB at sample end. */
17
+ peakRssKb?: number;
18
+ }): void;
19
+ private recordIntoMap;
20
+ private buildUnitsList;
21
+ hasData(): boolean;
22
+ totalDurationMs(): number;
23
+ /** Sum of measured process CPU ms in this window (0 if none measured). */
24
+ totalMeasuredCpuMs(): number;
25
+ /** True when any unit recorded measured process CPU (not duration proxy). */
26
+ hasMeasuredCpu(): boolean;
27
+ /** True when any unit recorded peak RSS. */
28
+ hasMeasuredRss(): boolean;
29
+ /**
30
+ * Build service_runtime_v0. N1 uses duration-share energy attribution (trend).
31
+ * CPU-accurate share can land later without changing the schema.
32
+ */
33
+ buildV0(opts: {
34
+ collector: string;
35
+ framework: string;
36
+ windowEnergyKwh: number;
37
+ windowSeconds: number;
38
+ }): ServiceRuntimeV0 | null;
39
+ /** Take a snapshot and reset live window (Python take_snapshot analogue). */
40
+ takeSnapshot(): ServiceRuntimeAggregator;
41
+ mergeSnapshot(snapshot: ServiceRuntimeAggregator): void;
42
+ private mergeMap;
43
+ }