@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,42 @@
1
+ /** @param {unknown} document @returns {{accepted: boolean, errorCodes: string[]}} */
2
+ export function validateObservationEnvelope(document: unknown): {
3
+ accepted: boolean;
4
+ errorCodes: string[];
5
+ };
6
+ /**
7
+ * Pair comparison only, with both complete documents validated first.
8
+ * Object order is ignored and array order preserved. Decoded number equality is
9
+ * provisional: this is not RFC 8785 or the still-open MP0 numeric/wire policy.
10
+ * @param {unknown} first
11
+ * @param {unknown} second
12
+ * @returns {{classification: 'duplicate'|'conflict'|'reject', errorCodes: string[]}}
13
+ */
14
+ export function classifyObservationDelivery(first: unknown, second: unknown): {
15
+ classification: "duplicate" | "conflict" | "reject";
16
+ errorCodes: string[];
17
+ };
18
+ /**
19
+ * Project semantic authoring envelope → Protobuf JSON view without mutating input.
20
+ * @param {unknown} observation
21
+ * @returns {{accepted: boolean, document: object|null, errorCodes: string[]}}
22
+ */
23
+ export function projectSemanticEnvelopeToProtobufView(observation: unknown): {
24
+ accepted: boolean;
25
+ document: object | null;
26
+ errorCodes: string[];
27
+ };
28
+ /** @param {unknown} document @returns {{accepted: boolean, errorCodes: string[]}} */
29
+ export function validateObservationProtobufView(document: unknown): {
30
+ accepted: boolean;
31
+ errorCodes: string[];
32
+ };
33
+ /**
34
+ * Additive dual-view gate: semantic envelope must validate and project to a valid protobuf view.
35
+ * Does not replace validateObservationEnvelope for TCK.
36
+ * @param {unknown} observation
37
+ * @returns {{accepted: boolean, errorCodes: string[]}}
38
+ */
39
+ export function validateObservationDualView(observation: unknown): {
40
+ accepted: boolean;
41
+ errorCodes: string[];
42
+ };
@@ -0,0 +1,555 @@
1
+ /**
2
+ * Public producer-validation and pair-identity boundaries for the pinned MP0 draft.
3
+ * No collector, cloud upload, fixture oracle, or conformance result generation lives here.
4
+ */
5
+ import { readFileSync } from 'node:fs'
6
+ import { createRequire } from 'node:module'
7
+
8
+ const require = createRequire(import.meta.url)
9
+ let shapeValidator
10
+ let protobufViewValidator
11
+ function validateShape(document) {
12
+ // Legacy runtime imports should not compile or load the draft validator eagerly.
13
+ if (!shapeValidator) {
14
+ const Ajv2020 = require('ajv/dist/2020.js').default
15
+ const addFormats = require('ajv-formats').default
16
+ const schema = JSON.parse(readFileSync(new URL('./observation-envelope.schema.json', import.meta.url), 'utf8'))
17
+ // Disable strict schema-authoring warnings, not producer/format validation.
18
+ const ajv = new Ajv2020({ strict: false, strictNumbers: true, allErrors: false, ownProperties: true })
19
+ addFormats(ajv, ['date-time'])
20
+ shapeValidator = ajv.compile(schema)
21
+ }
22
+ return shapeValidator(document)
23
+ }
24
+ function validateProtobufViewShape(document) {
25
+ if (!protobufViewValidator) {
26
+ const Ajv2020 = require('ajv/dist/2020.js').default
27
+ const addFormats = require('ajv-formats').default
28
+ const schema = JSON.parse(readFileSync(new URL('./observation-protobuf-view.schema.json', import.meta.url), 'utf8'))
29
+ const ajv = new Ajv2020({ strict: false, strictNumbers: true, allErrors: false, ownProperties: true })
30
+ addFormats(ajv, ['date-time'])
31
+ protobufViewValidator = ajv.compile(schema)
32
+ }
33
+ return protobufViewValidator(document)
34
+ }
35
+
36
+ const PROTOCOL_MAJOR = 1
37
+ const PROTOCOL_MINOR = 0
38
+ /** Mirrors observation/v1/json-semantic-mapping.v1.json (dual-view migration). */
39
+ const ENUM_MAPS = {
40
+ deployment_mode: {
41
+ vm: 'DEPLOYMENT_MODE_VM', container: 'DEPLOYMENT_MODE_CONTAINER',
42
+ kubernetes: 'DEPLOYMENT_MODE_KUBERNETES', ci_job: 'DEPLOYMENT_MODE_CI_JOB',
43
+ serverless_thin: 'DEPLOYMENT_MODE_SERVERLESS_THIN', local: 'DEPLOYMENT_MODE_LOCAL',
44
+ unknown: 'DEPLOYMENT_MODE_UNKNOWN',
45
+ },
46
+ content_capture: { none: 'CONTENT_CAPTURE_NONE' },
47
+ dimension_policy: { allowlist: 'DIMENSION_POLICY_ALLOWLIST' },
48
+ route_policy: { template_or_unknown: 'ROUTE_POLICY_TEMPLATE_OR_UNKNOWN' },
49
+ window_state: { open: 'WINDOW_STATE_OPEN', closed: 'WINDOW_STATE_CLOSED', thin: 'WINDOW_STATE_THIN' },
50
+ clock_source: {
51
+ runtime_monotonic: 'CLOCK_SOURCE_RUNTIME_MONOTONIC',
52
+ process_monotonic: 'CLOCK_SOURCE_PROCESS_MONOTONIC',
53
+ unavailable: 'CLOCK_SOURCE_UNAVAILABLE',
54
+ },
55
+ operation_kind: {
56
+ 'http.server': 'OPERATION_KIND_HTTP_SERVER', task: 'OPERATION_KIND_TASK',
57
+ process: 'OPERATION_KIND_PROCESS', script: 'OPERATION_KIND_SCRIPT',
58
+ cron: 'OPERATION_KIND_CRON', background: 'OPERATION_KIND_BACKGROUND',
59
+ unattributed: 'OPERATION_KIND_UNATTRIBUTED',
60
+ },
61
+ operation_unit: {
62
+ http_route: 'OPERATION_UNIT_HTTP_ROUTE', task: 'OPERATION_UNIT_TASK',
63
+ process: 'OPERATION_UNIT_PROCESS', script: 'OPERATION_UNIT_SCRIPT',
64
+ cron: 'OPERATION_UNIT_CRON', background: 'OPERATION_UNIT_BACKGROUND',
65
+ unattributed: 'OPERATION_UNIT_UNATTRIBUTED',
66
+ },
67
+ identity_source: {
68
+ framework_template: 'IDENTITY_SOURCE_FRAMEWORK_TEMPLATE',
69
+ declared: 'IDENTITY_SOURCE_DECLARED', generated: 'IDENTITY_SOURCE_GENERATED',
70
+ unknown_bucket: 'IDENTITY_SOURCE_UNKNOWN_BUCKET',
71
+ },
72
+ operation_phase: {
73
+ start: 'OPERATION_PHASE_START', finish: 'OPERATION_PHASE_FINISH',
74
+ heartbeat: 'OPERATION_PHASE_HEARTBEAT',
75
+ },
76
+ outcome: {
77
+ ok: 'OUTCOME_OK', error: 'OUTCOME_ERROR', cancelled: 'OUTCOME_CANCELLED',
78
+ unknown: 'OUTCOME_UNKNOWN',
79
+ },
80
+ usage_source: {
81
+ provider_response: 'USAGE_SOURCE_PROVIDER_RESPONSE',
82
+ documented_header: 'USAGE_SOURCE_DOCUMENTED_HEADER',
83
+ sdk_callback: 'USAGE_SOURCE_SDK_CALLBACK', declared: 'USAGE_SOURCE_DECLARED',
84
+ },
85
+ resource_scope: {
86
+ process: 'RESOURCE_SCOPE_PROCESS', process_tree: 'RESOURCE_SCOPE_PROCESS_TREE',
87
+ container: 'RESOURCE_SCOPE_CONTAINER', cgroup: 'RESOURCE_SCOPE_CGROUP',
88
+ host: 'RESOURCE_SCOPE_HOST', device: 'RESOURCE_SCOPE_DEVICE',
89
+ },
90
+ sample_kind: {
91
+ start: 'SAMPLE_KIND_START', periodic: 'SAMPLE_KIND_PERIODIC',
92
+ end: 'SAMPLE_KIND_END', snapshot: 'SAMPLE_KIND_SNAPSHOT',
93
+ },
94
+ measurement_name: {
95
+ 'process.cpu.time': 'MEASUREMENT_NAME_PROCESS_CPU_TIME',
96
+ 'process.memory.rss': 'MEASUREMENT_NAME_PROCESS_MEMORY_RSS',
97
+ 'system.energy': 'MEASUREMENT_NAME_SYSTEM_ENERGY',
98
+ 'gpu.energy': 'MEASUREMENT_NAME_GPU_ENERGY',
99
+ 'gpu.utilization': 'MEASUREMENT_NAME_GPU_UTILIZATION',
100
+ },
101
+ measurement_unit: {
102
+ ns: 'MEASUREMENT_UNIT_NS', By: 'MEASUREMENT_UNIT_BY',
103
+ J: 'MEASUREMENT_UNIT_J', '1': 'MEASUREMENT_UNIT_RATIO',
104
+ },
105
+ temporality: {
106
+ cumulative: 'TEMPORALITY_CUMULATIVE', delta: 'TEMPORALITY_DELTA', gauge: 'TEMPORALITY_GAUGE',
107
+ },
108
+ health_component: {
109
+ adapter: 'HEALTH_COMPONENT_ADAPTER', exporter: 'HEALTH_COMPONENT_EXPORTER',
110
+ sampler: 'HEALTH_COMPONENT_SAMPLER', collector: 'HEALTH_COMPONENT_COLLECTOR',
111
+ },
112
+ health_status: {
113
+ ok: 'HEALTH_STATUS_OK', degraded: 'HEALTH_STATUS_DEGRADED',
114
+ error: 'HEALTH_STATUS_ERROR', disabled: 'HEALTH_STATUS_DISABLED',
115
+ },
116
+ }
117
+
118
+ const contentFields = new Set(['authorization', 'body', 'completion', 'cookie', 'database_statement', 'error_text', 'headers', 'prompt', 'request_body', 'response_body', 'secret'])
119
+ const derivedFields = new Set(['carbon', 'carbon_factor', 'carbon_g', 'co2e', 'cost', 'cost_usd', 'currency', 'energy_estimate', 'energy_kwh', 'estimated_energy', 'estimated_energy_j', 'price', 'pricing'])
120
+ const knownCapabilities = new Set(['CAP-R1', 'CAP-R2', 'CAP-R3', 'CAP-R4', 'CAP-R5', 'CAP-R6', 'CAP-A1', 'CAP-A2', 'CAP-A3', 'CAP-O1', 'CAP-O2', 'CAP-P1', 'CAP-P2', 'CAP-Q1'])
121
+ const operationKinds = new Set(['http.server', 'task', 'process', 'script', 'cron', 'background', 'unattributed'])
122
+ const recordCapabilities = new Map([['external_call', 'CAP-R3'], ['resource_sample', 'CAP-R4'], ['adapter_health', 'CAP-R5']])
123
+ const measurementShapes = new Map([
124
+ ['process.cpu.time', ['ns', ['cumulative', 'delta']]],
125
+ ['process.memory.rss', ['By', ['gauge']]],
126
+ ['system.energy', ['J', ['cumulative', 'delta']]],
127
+ ['gpu.energy', ['J', ['cumulative', 'delta']]],
128
+ ['gpu.utilization', ['1', ['gauge']]],
129
+ ])
130
+ const uint64Max = (1n << 64n) - 1n
131
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
132
+ const array = (value) => Array.isArray(value) ? value : []
133
+ const has = (value, key) => Object.hasOwn(value, key)
134
+ const integer = (value) => {
135
+ if (typeof value === 'string' && /^-?[0-9]+$/.test(value)) return BigInt(value)
136
+ if (typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value)) return BigInt(value)
137
+ return null
138
+ }
139
+
140
+ function checkRange(value, codes) {
141
+ const parsed = integer(value)
142
+ if (parsed !== null && (parsed < 0n || parsed > uint64Max)) codes.add('UINT64_OUT_OF_RANGE')
143
+ }
144
+
145
+ function repeated(values) {
146
+ return new Set(values).size !== values.length
147
+ }
148
+
149
+ function strictTimestamp(value) {
150
+ // Match the draft Python checker on leap seconds/year zero, while rejecting its
151
+ // trailing-newline quirk. Calendar validity remains the schema format check.
152
+ return typeof value === 'string' && !/\s/.test(value) &&
153
+ /^(?!0000)[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt][0-9]{2}:[0-9]{2}:[0-5][0-9](?:\.[0-9]+)?(?:[Zz]|[+-][0-9]{2}:[0-9]{2})$/.test(value)
154
+ }
155
+
156
+ // Reject native values/cycles before handing an arbitrary caller's input to Ajv.
157
+ // Error codes never contain customer values or validation-library error messages.
158
+ function inspectJson(value, codes, ancestors = new Set(), depth = 0) {
159
+ if (depth > 64) return false
160
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return true
161
+ if (typeof value === 'number') {
162
+ if (!Number.isFinite(value)) { codes.add('NONFINITE_NUMBER'); return false }
163
+ return true
164
+ }
165
+ if (!object(value) && !Array.isArray(value)) return false
166
+ if (ancestors.has(value)) return false
167
+ if (!Array.isArray(value) && ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return false
168
+ ancestors.add(value)
169
+ let valid = true
170
+ for (const [key, child] of Object.entries(value)) {
171
+ const lower = key.toLowerCase()
172
+ if (contentFields.has(lower)) codes.add('FORBIDDEN_CONTENT_FIELD')
173
+ if (derivedFields.has(lower)) codes.add('FORBIDDEN_DERIVED_FIELD')
174
+ valid = inspectJson(child, codes, ancestors, depth + 1) && valid
175
+ }
176
+ ancestors.delete(value)
177
+ return valid
178
+ }
179
+
180
+ function checkOperation(operation, codes) {
181
+ if (!object(operation)) return
182
+ const expectedUnit = operation.kind === 'http.server' ? 'http_route' : operation.kind
183
+ if (operationKinds.has(operation.kind) && operation.unit_type !== expectedUnit) codes.add('OPERATION_KIND_UNIT_MISMATCH')
184
+ if (operation.kind !== 'http.server') return
185
+ const source = operation.identity_source
186
+ const route = operation.route_template
187
+ const name = operation.name
188
+ const allowedSource = ['framework_template', 'declared', 'generated', 'unknown_bucket'].includes(source)
189
+ const rawRoute = typeof route !== 'string' || /[?#]/.test(route) ||
190
+ (source === 'generated' && /(?:^|\/)(?:[0-9]{4,}|[0-9a-f]{8}-[0-9a-f-]{27,})(?:\/|$)/i.test(route))
191
+ if (!allowedSource || rawRoute || (typeof name === 'string' && /[?#]/.test(name)) ||
192
+ (source === 'unknown_bucket' && route !== '__unknown__') ||
193
+ (route === '__unknown__' && source !== 'unknown_bucket')) codes.add('RAW_ROUTE_IDENTITY_FORBIDDEN')
194
+ }
195
+
196
+ function checkFacts(event, codes) {
197
+ const facts = event.facts
198
+ if (!object(facts)) return
199
+ if (event.record_type === 'operation_summary') {
200
+ if (Number.isInteger(facts.calls) && Number.isInteger(facts.errors) && facts.errors > facts.calls) codes.add('INVALID_COUNTER_RELATIONSHIP')
201
+ const sum = integer(facts.duration_ns_sum)
202
+ const max = integer(facts.duration_ns_max)
203
+ if (sum !== null && max !== null && max > sum) codes.add('INVALID_DURATION_RELATIONSHIP')
204
+ checkRange(facts.duration_ns_sum, codes)
205
+ checkRange(facts.duration_ns_max, codes)
206
+ } else if (event.record_type === 'operation_event') {
207
+ if (typeof event.execution_id !== 'string') codes.add('LIFECYCLE_EXECUTION_ID_REQUIRED')
208
+ if ((facts.phase === 'start' && (facts.outcome !== 'unknown' || has(facts, 'duration_ns'))) ||
209
+ (facts.phase === 'finish' && !has(facts, 'duration_ns'))) codes.add('LIFECYCLE_PHASE_FACT_MISMATCH')
210
+ if (has(facts, 'duration_ns')) checkRange(facts.duration_ns, codes)
211
+ } else if (event.record_type === 'external_call') {
212
+ if (Number.isInteger(facts.attempts) && Number.isInteger(facts.errors) && facts.errors > facts.attempts) codes.add('INVALID_COUNTER_RELATIONSHIP')
213
+ checkRange(facts.duration_ns, codes)
214
+ const usage = array(facts.usage).filter(object)
215
+ if (repeated(usage.map((entry) => entry.unit))) codes.add('DUPLICATE_USAGE_UNIT')
216
+ for (const entry of usage) checkRange(entry.value, codes)
217
+ } else if (event.record_type === 'resource_sample') {
218
+ const measurements = array(facts.measurements).filter(object)
219
+ if (repeated(measurements.map((entry) => entry.name))) codes.add('DUPLICATE_MEASUREMENT')
220
+ for (const entry of measurements) {
221
+ const shape = measurementShapes.get(entry.name)
222
+ if (shape && (entry.unit !== shape[0] || !shape[1].includes(entry.temporality))) codes.add('INVALID_MEASUREMENT_SHAPE')
223
+ if (entry.name === 'process.cpu.time') {
224
+ checkRange(entry.value, codes)
225
+ if (/request_duration|wall|elapsed|proxy/i.test(String(entry.source ?? ''))) codes.add('CPU_SOURCE_WALL_DURATION')
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ /** @param {unknown} document @returns {{accepted: boolean, errorCodes: string[]}} */
232
+ export function validateObservationEnvelope(document) {
233
+ const codes = new Set()
234
+ try {
235
+ if (!inspectJson(document, codes)) {
236
+ codes.add('SCHEMA_VALIDATION_ERROR')
237
+ return { accepted: false, errorCodes: [...codes].sort() }
238
+ }
239
+ if (!validateShape(document)) codes.add('SCHEMA_VALIDATION_ERROR')
240
+ if (!object(document)) return { accepted: false, errorCodes: [...codes].sort() }
241
+ if (!strictTimestamp(document.emitted_at)) codes.add('SCHEMA_VALIDATION_ERROR')
242
+ if (object(document.window)) {
243
+ if (!strictTimestamp(document.window.start_time) ||
244
+ (document.window.end_time != null && !strictTimestamp(document.window.end_time))) codes.add('SCHEMA_VALIDATION_ERROR')
245
+ }
246
+ checkRange(document.batch_sequence, codes)
247
+ const capabilities = new Set(array(document.source?.capability_ids).filter((id) => typeof id === 'string'))
248
+ if ([...capabilities].some((id) => !knownCapabilities.has(id))) codes.add('UNKNOWN_CAPABILITY_ID')
249
+ const events = array(document.events).filter(object)
250
+ const ids = events.map((event) => event.event_id).filter((id) => typeof id === 'string')
251
+ const idSet = new Set(ids)
252
+ const sequences = events.map((event) => integer(event.sequence)).filter((value) => value !== null)
253
+ if (repeated(ids)) codes.add('DUPLICATE_EVENT_ID')
254
+ if (repeated(sequences)) codes.add('DUPLICATE_SEQUENCE')
255
+ if (sequences.some((value, index) => index > 0 && value <= sequences[index - 1])) codes.add('INVALID_SEQUENCE_ORDER')
256
+ for (const event of events) {
257
+ for (const field of ['sequence', 'wall_time_unix_nano', 'monotonic_time_ns']) checkRange(event[field], codes)
258
+ if (typeof event.traceparent === 'string') {
259
+ const parts = event.traceparent.split('-')
260
+ if (parts.length !== 4 || parts[0] === 'ff' || parts[1] === '0'.repeat(32) || parts[2] === '0'.repeat(16)) codes.add('INVALID_TRACEPARENT')
261
+ }
262
+ if (event.parent_operation_event_id != null && !idSet.has(event.parent_operation_event_id)) codes.add('UNKNOWN_PARENT_EVENT')
263
+ checkOperation(event.operation, codes)
264
+ checkFacts(event, codes)
265
+ if (object(event.facts)) {
266
+ const capability = ['operation_event', 'operation_summary'].includes(event.record_type)
267
+ ? (event.operation?.kind === 'http.server' ? 'CAP-R1' : 'CAP-R2')
268
+ : recordCapabilities.get(event.record_type)
269
+ if (capability && !capabilities.has(capability)) codes.add('SOURCE_CAPABILITY_MISMATCH')
270
+ }
271
+ const windowId = object(document.window) ? document.window.runtime_window_id : null
272
+ if (windowId) {
273
+ if (event.runtime_window_id == null) codes.add('WINDOW_ID_MISSING')
274
+ else if (event.runtime_window_id !== windowId) codes.add('WINDOW_ID_MISMATCH')
275
+ }
276
+ if (event.clock_source === 'unavailable' && event.monotonic_time_ns !== '0') codes.add('CLOCK_SOURCE_MISMATCH')
277
+ }
278
+ const window = document.window
279
+ if (object(window) && ((window.state === 'open' && window.end_time != null) ||
280
+ (window.state === 'closed' && window.end_time == null))) codes.add('WINDOW_STATE_TIME_MISMATCH')
281
+ } catch {
282
+ codes.add('SCHEMA_VALIDATION_ERROR')
283
+ }
284
+ return { accepted: codes.size === 0, errorCodes: [...codes].sort() }
285
+ }
286
+
287
+ function semanticEqual(left, right) {
288
+ if (left === right) return true
289
+ if (Array.isArray(left) || Array.isArray(right)) {
290
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length &&
291
+ left.every((value, index) => semanticEqual(value, right[index]))
292
+ }
293
+ if (!object(left) || !object(right)) return false
294
+ const keys = Object.keys(left)
295
+ return keys.length === Object.keys(right).length && keys.every((key) => has(right, key) && semanticEqual(left[key], right[key]))
296
+ }
297
+
298
+ /**
299
+ * Pair comparison only, with both complete documents validated first.
300
+ * Object order is ignored and array order preserved. Decoded number equality is
301
+ * provisional: this is not RFC 8785 or the still-open MP0 numeric/wire policy.
302
+ * @param {unknown} first
303
+ * @param {unknown} second
304
+ * @returns {{classification: 'duplicate'|'conflict'|'reject', errorCodes: string[]}}
305
+ */
306
+ export function classifyObservationDelivery(first, second) {
307
+ const errorCodes = [...new Set([
308
+ ...validateObservationEnvelope(first).errorCodes,
309
+ ...validateObservationEnvelope(second).errorCodes,
310
+ ])].sort()
311
+ if (errorCodes.length) return { classification: 'reject', errorCodes }
312
+ if (first.batch_id !== second.batch_id) return { classification: 'conflict', errorCodes: ['PAIR_BATCH_ID_MISMATCH'] }
313
+ return semanticEqual(first, second)
314
+ ? { classification: 'duplicate', errorCodes: [] }
315
+ : { classification: 'conflict', errorCodes: ['ID_PAYLOAD_CONFLICT'] }
316
+ }
317
+
318
+ function mapEnum(table, value, codes) {
319
+ const mapped = typeof value === 'string' ? ENUM_MAPS[table][value] : undefined
320
+ if (mapped === undefined) codes.add('PROJECTION_UNMAPPED_ENUM')
321
+ return mapped
322
+ }
323
+
324
+ function asUint64String(value, codes, required = false) {
325
+ if (value == null) {
326
+ if (required) codes.add('PROJECTION_SHAPE_ERROR')
327
+ return undefined
328
+ }
329
+ if (typeof value === 'boolean') {
330
+ codes.add('PROJECTION_SHAPE_ERROR')
331
+ return undefined
332
+ }
333
+ if (typeof value === 'number' && Number.isInteger(value) && value >= 0) return String(value)
334
+ if (typeof value === 'string' && /^[0-9]+$/.test(value)) return value
335
+ codes.add('PROJECTION_SHAPE_ERROR')
336
+ return undefined
337
+ }
338
+
339
+ function asCoverageString(value, codes) {
340
+ if (typeof value === 'boolean') {
341
+ codes.add('PROJECTION_SHAPE_ERROR')
342
+ return undefined
343
+ }
344
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1) {
345
+ if (value === 0 || value === 1) return String(value)
346
+ const text = value.toFixed(12).replace(/0+$/, '').replace(/\.$/, '')
347
+ if (!/^0\.[0-9]{0,11}[1-9]$/.test(text)) {
348
+ codes.add('PROJECTION_SHAPE_ERROR')
349
+ return undefined
350
+ }
351
+ return text
352
+ }
353
+ if (typeof value === 'string' && /^(0|1|0\.[0-9]{0,11}[1-9])$/.test(value)) return value
354
+ codes.add('PROJECTION_SHAPE_ERROR')
355
+ return undefined
356
+ }
357
+
358
+ function projectOperation(operation, codes) {
359
+ const projected = {
360
+ kind: mapEnum('operation_kind', operation.kind, codes),
361
+ unit_type: mapEnum('operation_unit', operation.unit_type, codes),
362
+ name: operation.name,
363
+ identity_source: mapEnum('identity_source', operation.identity_source, codes),
364
+ }
365
+ if (has(operation, 'http_method')) projected.http_method = operation.http_method
366
+ if (has(operation, 'route_template')) projected.route_template = operation.route_template
367
+ return projected
368
+ }
369
+
370
+ function projectEvent(event, codes) {
371
+ const projected = {
372
+ event_id: event.event_id,
373
+ sequence: asUint64String(event.sequence, codes, true),
374
+ wall_time_unix_nano: asUint64String(event.wall_time_unix_nano, codes, true),
375
+ clock_source: mapEnum('clock_source', event.clock_source, codes),
376
+ }
377
+ if (has(event, 'monotonic_time_ns')) projected.monotonic_time_ns = asUint64String(event.monotonic_time_ns, codes)
378
+ for (const key of ['execution_id', 'runtime_window_id', 'traceparent', 'clock_epoch_id']) {
379
+ if (event[key] != null) projected[key] = event[key]
380
+ }
381
+ const facts = object(event.facts) ? event.facts : {}
382
+ const parent = event.parent_operation_event_id
383
+ if (event.record_type === 'operation_event') {
384
+ const body = {
385
+ operation: projectOperation(object(event.operation) ? event.operation : {}, codes),
386
+ phase: mapEnum('operation_phase', facts.phase, codes),
387
+ }
388
+ if (has(facts, 'outcome')) body.outcome = mapEnum('outcome', facts.outcome, codes)
389
+ if (has(facts, 'duration_ns')) body.duration_ns = asUint64String(facts.duration_ns, codes)
390
+ if (facts.status_code != null) body.status_code = facts.status_code
391
+ projected.operation_event = body
392
+ } else if (event.record_type === 'operation_summary') {
393
+ projected.operation_summary = {
394
+ operation: projectOperation(object(event.operation) ? event.operation : {}, codes),
395
+ calls: asUint64String(facts.calls, codes, true),
396
+ errors: asUint64String(facts.errors, codes, true),
397
+ duration_ns_sum: asUint64String(facts.duration_ns_sum, codes, true),
398
+ duration_ns_max: asUint64String(facts.duration_ns_max, codes, true),
399
+ ...(facts.concurrency_max != null ? { concurrency_max: facts.concurrency_max } : {}),
400
+ }
401
+ } else if (event.record_type === 'external_call') {
402
+ const body = {
403
+ service: facts.service,
404
+ operation: facts.operation,
405
+ attempts: asUint64String(facts.attempts, codes, true),
406
+ errors: asUint64String(facts.errors, codes, true),
407
+ duration_ns: asUint64String(facts.duration_ns, codes, true),
408
+ }
409
+ if (parent != null) body.parent_operation_event_id = parent
410
+ for (const key of ['provider', 'model', 'destination_domain']) {
411
+ if (facts[key] != null) body[key] = facts[key]
412
+ }
413
+ if (has(facts, 'usage')) {
414
+ body.usage = array(facts.usage).filter(object).map((entry) => ({
415
+ unit: entry.unit,
416
+ value: asUint64String(entry.value, codes, true),
417
+ source: mapEnum('usage_source', entry.source, codes),
418
+ }))
419
+ }
420
+ projected.external_call = body
421
+ } else if (event.record_type === 'resource_sample') {
422
+ const measurements = array(facts.measurements).filter(object).map((entry) => {
423
+ const measurement = {
424
+ name: mapEnum('measurement_name', entry.name, codes),
425
+ value: typeof entry.value === 'string' ? entry.value
426
+ : (typeof entry.value === 'number' && Number.isFinite(entry.value) ? String(entry.value) : undefined),
427
+ unit: mapEnum('measurement_unit', entry.unit, codes),
428
+ temporality: mapEnum('temporality', entry.temporality, codes),
429
+ source: entry.source,
430
+ coverage: asCoverageString(entry.coverage, codes),
431
+ }
432
+ if (measurement.value === undefined) codes.add('PROJECTION_SHAPE_ERROR')
433
+ if (entry.counter_epoch_id != null) measurement.counter_epoch_id = entry.counter_epoch_id
434
+ return measurement
435
+ })
436
+ const body = {
437
+ scope: mapEnum('resource_scope', facts.scope, codes),
438
+ sample_kind: mapEnum('sample_kind', facts.sample_kind, codes),
439
+ subject_id: facts.subject_id,
440
+ measurements,
441
+ }
442
+ if (parent != null) body.parent_operation_event_id = parent
443
+ projected.resource_sample = body
444
+ } else if (event.record_type === 'adapter_health') {
445
+ const countersIn = object(facts.counters) ? facts.counters : {}
446
+ const counters = Object.fromEntries(
447
+ Object.keys(countersIn).map((key) => [key, asUint64String(countersIn[key], codes, true)]),
448
+ )
449
+ projected.adapter_health = {
450
+ component: mapEnum('health_component', facts.component, codes),
451
+ status: mapEnum('health_status', facts.status, codes),
452
+ reason_codes: array(facts.reason_codes),
453
+ counters,
454
+ }
455
+ } else {
456
+ codes.add('PROJECTION_SHAPE_ERROR')
457
+ }
458
+ return projected
459
+ }
460
+
461
+ /**
462
+ * Project semantic authoring envelope → Protobuf JSON view without mutating input.
463
+ * @param {unknown} observation
464
+ * @returns {{accepted: boolean, document: object|null, errorCodes: string[]}}
465
+ */
466
+ export function projectSemanticEnvelopeToProtobufView(observation) {
467
+ const codes = new Set()
468
+ if (!object(observation)) {
469
+ return { accepted: false, document: null, errorCodes: ['PROJECTION_SHAPE_ERROR'] }
470
+ }
471
+ try {
472
+ const source = object(observation.source) ? observation.source : {}
473
+ const routing = object(observation.routing) ? observation.routing : {}
474
+ const privacy = object(observation.privacy) ? observation.privacy : {}
475
+ const document = {
476
+ protocol_major: PROTOCOL_MAJOR,
477
+ protocol_minor: PROTOCOL_MINOR,
478
+ batch_id: observation.batch_id,
479
+ batch_sequence: asUint64String(observation.batch_sequence, codes, true),
480
+ emitted_at: observation.emitted_at,
481
+ source: {
482
+ language: source.language,
483
+ runtime_name: source.runtime_name,
484
+ runtime_version: source.runtime_version,
485
+ adapter_name: source.adapter_name,
486
+ adapter_version: source.adapter_version,
487
+ runtime_instance_id: source.runtime_instance_id,
488
+ capability_ids: array(source.capability_ids).filter((id) => typeof id === 'string'),
489
+ deployment_mode: mapEnum('deployment_mode', source.deployment_mode, codes),
490
+ ...(source.framework != null ? { framework: source.framework } : {}),
491
+ },
492
+ routing: {
493
+ project_id: asUint64String(routing.project_id, codes, true),
494
+ workload_id: routing.workload_id,
495
+ service_name: routing.service_name,
496
+ ...(routing.organization_id != null
497
+ ? { organization_id: asUint64String(routing.organization_id, codes, true) }
498
+ : {}),
499
+ ...(routing.environment != null ? { environment: routing.environment } : {}),
500
+ ...(routing.release_tag != null ? { release_tag: routing.release_tag } : {}),
501
+ },
502
+ privacy: {
503
+ content_capture: mapEnum('content_capture', privacy.content_capture, codes),
504
+ dimension_policy: mapEnum('dimension_policy', privacy.dimension_policy, codes),
505
+ route_policy: mapEnum('route_policy', privacy.route_policy, codes),
506
+ dropped_dimension_count: asUint64String(privacy.dropped_dimension_count, codes, true),
507
+ },
508
+ events: array(observation.events).filter(object).map((event) => projectEvent(event, codes)),
509
+ }
510
+ if (object(observation.window)) {
511
+ const window = observation.window
512
+ document.window = {
513
+ state: mapEnum('window_state', window.state, codes),
514
+ start_time: window.start_time,
515
+ ...(window.runtime_window_id != null ? { runtime_window_id: window.runtime_window_id } : {}),
516
+ ...(window.end_time != null ? { end_time: window.end_time } : {}),
517
+ }
518
+ }
519
+ if (codes.size) return { accepted: false, document: null, errorCodes: [...codes].sort() }
520
+ return { accepted: true, document, errorCodes: [] }
521
+ } catch {
522
+ return { accepted: false, document: null, errorCodes: ['PROJECTION_SHAPE_ERROR'] }
523
+ }
524
+ }
525
+
526
+ /** @param {unknown} document @returns {{accepted: boolean, errorCodes: string[]}} */
527
+ export function validateObservationProtobufView(document) {
528
+ const codes = new Set()
529
+ try {
530
+ if (!inspectJson(document, codes)) {
531
+ codes.add('SCHEMA_VALIDATION_ERROR')
532
+ return { accepted: false, errorCodes: [...codes].sort() }
533
+ }
534
+ if (!validateProtobufViewShape(document)) codes.add('SCHEMA_VALIDATION_ERROR')
535
+ } catch {
536
+ codes.add('SCHEMA_VALIDATION_ERROR')
537
+ }
538
+ return { accepted: codes.size === 0, errorCodes: [...codes].sort() }
539
+ }
540
+
541
+ /**
542
+ * Additive dual-view gate: semantic envelope must validate and project to a valid protobuf view.
543
+ * Does not replace validateObservationEnvelope for TCK.
544
+ * @param {unknown} observation
545
+ * @returns {{accepted: boolean, errorCodes: string[]}}
546
+ */
547
+ export function validateObservationDualView(observation) {
548
+ const envelope = validateObservationEnvelope(observation)
549
+ if (!envelope.accepted) return envelope
550
+ const projected = projectSemanticEnvelopeToProtobufView(observation)
551
+ if (!projected.accepted || !projected.document) {
552
+ return { accepted: false, errorCodes: projected.errorCodes.length ? projected.errorCodes : ['PROJECTION_SHAPE_ERROR'] }
553
+ }
554
+ return validateObservationProtobufView(projected.document)
555
+ }
@@ -0,0 +1,11 @@
1
+ export declare const DOMAINS: {
2
+ readonly runtime_instance: "netgreener.observation.v1/runtime-instance";
3
+ readonly event: "netgreener.observation.v1/event";
4
+ readonly batch: "netgreener.observation.v1/batch";
5
+ readonly runtime_window: "netgreener.observation.v1/runtime-window";
6
+ };
7
+ export declare function lengthPrefixed(parts: Array<string | Buffer>): Buffer;
8
+ export declare function identityDigest(domain: string, ...components: string[]): string;
9
+ export declare function formatId(prefix: string, digestHex: string): string;
10
+ export declare function identityId(prefix: string, domain: string, components: string[]): string;
11
+ export declare function prefixForDomain(domain: string): string;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Observation identity-preimage helpers (ADR 0008 D3 twin).
3
+ *
4
+ * Additive helper for golden parity. Does not mint IDs in emit/upload paths.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ export const DOMAINS = {
8
+ runtime_instance: 'netgreener.observation.v1/runtime-instance',
9
+ event: 'netgreener.observation.v1/event',
10
+ batch: 'netgreener.observation.v1/batch',
11
+ runtime_window: 'netgreener.observation.v1/runtime-window',
12
+ };
13
+ const PREFIX_BY_DOMAIN = {
14
+ [DOMAINS.runtime_instance]: 'rti',
15
+ [DOMAINS.event]: 'evt',
16
+ [DOMAINS.batch]: 'bat',
17
+ [DOMAINS.runtime_window]: 'rtw',
18
+ };
19
+ export function lengthPrefixed(parts) {
20
+ const chunks = [];
21
+ for (const part of parts) {
22
+ const raw = typeof part === 'string' ? Buffer.from(part, 'utf8') : part;
23
+ const header = Buffer.alloc(4);
24
+ header.writeUInt32BE(raw.length, 0);
25
+ chunks.push(header, raw);
26
+ }
27
+ return Buffer.concat(chunks);
28
+ }
29
+ export function identityDigest(domain, ...components) {
30
+ return createHash('sha256')
31
+ .update(lengthPrefixed([domain, ...components]))
32
+ .digest('hex');
33
+ }
34
+ export function formatId(prefix, digestHex) {
35
+ if (digestHex.length !== 64 || !/^[0-9a-f]+$/.test(digestHex)) {
36
+ throw new Error('digest must be 64 lowercase hex chars');
37
+ }
38
+ return `${prefix}_${digestHex}`;
39
+ }
40
+ export function identityId(prefix, domain, components) {
41
+ return formatId(prefix, identityDigest(domain, ...components));
42
+ }
43
+ export function prefixForDomain(domain) {
44
+ const prefix = PREFIX_BY_DOMAIN[domain];
45
+ if (!prefix)
46
+ throw new Error(`unknown identity domain: ${domain}`);
47
+ return prefix;
48
+ }
@@ -0,0 +1,3 @@
1
+ export declare function projectValue(value: unknown): unknown;
2
+ export declare function projectionBytes(document: Record<string, unknown>): Buffer;
3
+ export declare function projectionDigest(document: Record<string, unknown>): string;