apcore-js 0.5.0 → 0.7.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 (319) hide show
  1. package/README.md +1 -1
  2. package/dist/acl.d.ts +27 -0
  3. package/dist/acl.d.ts.map +1 -0
  4. package/dist/acl.js +175 -0
  5. package/dist/acl.js.map +1 -0
  6. package/dist/approval.d.ts +85 -0
  7. package/dist/approval.d.ts.map +1 -0
  8. package/dist/approval.js +73 -0
  9. package/dist/approval.js.map +1 -0
  10. package/dist/async-task.d.ts +90 -0
  11. package/dist/async-task.d.ts.map +1 -0
  12. package/dist/async-task.js +215 -0
  13. package/dist/async-task.js.map +1 -0
  14. package/dist/bindings.d.ts +12 -0
  15. package/dist/bindings.d.ts.map +1 -0
  16. package/dist/bindings.js +185 -0
  17. package/dist/bindings.js.map +1 -0
  18. package/dist/cancel.d.ts +14 -0
  19. package/dist/cancel.d.ts.map +1 -0
  20. package/dist/cancel.js +27 -0
  21. package/dist/cancel.js.map +1 -0
  22. package/dist/config.d.ts +9 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +23 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/context.d.ts +50 -0
  27. package/dist/context.d.ts.map +1 -0
  28. package/dist/context.js +87 -0
  29. package/dist/context.js.map +1 -0
  30. package/dist/decorator.d.ts +57 -0
  31. package/dist/decorator.d.ts.map +1 -0
  32. package/dist/decorator.js +74 -0
  33. package/dist/decorator.js.map +1 -0
  34. package/dist/errors.d.ts +204 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +364 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/executor.d.ts +82 -0
  39. package/dist/executor.d.ts.map +1 -0
  40. package/dist/executor.js +489 -0
  41. package/dist/executor.js.map +1 -0
  42. package/dist/extensions.d.ts +58 -0
  43. package/dist/extensions.d.ts.map +1 -0
  44. package/dist/extensions.js +239 -0
  45. package/dist/extensions.js.map +1 -0
  46. package/{src/index.ts → dist/index.d.ts} +6 -63
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +45 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/middleware/adapters.d.ts +18 -0
  51. package/dist/middleware/adapters.d.ts.map +1 -0
  52. package/dist/middleware/adapters.js +25 -0
  53. package/dist/middleware/adapters.js.map +1 -0
  54. package/dist/middleware/base.d.ts +10 -0
  55. package/dist/middleware/base.d.ts.map +1 -0
  56. package/dist/middleware/base.js +15 -0
  57. package/dist/middleware/base.js.map +1 -0
  58. package/{src/middleware/index.ts → dist/middleware/index.d.ts} +1 -0
  59. package/dist/middleware/index.d.ts.map +1 -0
  60. package/dist/middleware/index.js +5 -0
  61. package/dist/middleware/index.js.map +1 -0
  62. package/dist/middleware/logging.d.ts +25 -0
  63. package/dist/middleware/logging.d.ts.map +1 -0
  64. package/dist/middleware/logging.js +64 -0
  65. package/dist/middleware/logging.js.map +1 -0
  66. package/dist/middleware/manager.d.ts +21 -0
  67. package/dist/middleware/manager.d.ts.map +1 -0
  68. package/dist/middleware/manager.js +77 -0
  69. package/dist/middleware/manager.js.map +1 -0
  70. package/dist/module.d.ts +31 -0
  71. package/dist/module.d.ts.map +1 -0
  72. package/dist/module.js +12 -0
  73. package/dist/module.js.map +1 -0
  74. package/dist/observability/context-logger.d.ts +54 -0
  75. package/dist/observability/context-logger.d.ts.map +1 -0
  76. package/dist/observability/context-logger.js +151 -0
  77. package/dist/observability/context-logger.js.map +1 -0
  78. package/{src/observability/index.ts → dist/observability/index.d.ts} +1 -0
  79. package/dist/observability/index.d.ts.map +1 -0
  80. package/dist/observability/index.js +4 -0
  81. package/dist/observability/index.js.map +1 -0
  82. package/dist/observability/metrics.d.ts +30 -0
  83. package/dist/observability/metrics.d.ts.map +1 -0
  84. package/dist/observability/metrics.js +177 -0
  85. package/dist/observability/metrics.js.map +1 -0
  86. package/dist/observability/tracing.d.ts +62 -0
  87. package/dist/observability/tracing.d.ts.map +1 -0
  88. package/dist/observability/tracing.js +184 -0
  89. package/dist/observability/tracing.js.map +1 -0
  90. package/dist/registry/dependencies.d.ts +6 -0
  91. package/dist/registry/dependencies.d.ts.map +1 -0
  92. package/dist/registry/dependencies.js +83 -0
  93. package/dist/registry/dependencies.js.map +1 -0
  94. package/dist/registry/entry-point.d.ts +6 -0
  95. package/dist/registry/entry-point.d.ts.map +1 -0
  96. package/dist/registry/entry-point.js +55 -0
  97. package/dist/registry/entry-point.js.map +1 -0
  98. package/{src/registry/index.ts → dist/registry/index.d.ts} +1 -0
  99. package/dist/registry/index.d.ts.map +1 -0
  100. package/dist/registry/index.js +8 -0
  101. package/dist/registry/index.js.map +1 -0
  102. package/dist/registry/metadata.d.ts +9 -0
  103. package/dist/registry/metadata.d.ts.map +1 -0
  104. package/dist/registry/metadata.js +105 -0
  105. package/dist/registry/metadata.js.map +1 -0
  106. package/dist/registry/registry.d.ts +102 -0
  107. package/dist/registry/registry.d.ts.map +1 -0
  108. package/dist/registry/registry.js +534 -0
  109. package/dist/registry/registry.js.map +1 -0
  110. package/dist/registry/scanner.d.ts +7 -0
  111. package/dist/registry/scanner.d.ts.map +1 -0
  112. package/dist/registry/scanner.js +164 -0
  113. package/dist/registry/scanner.js.map +1 -0
  114. package/dist/registry/schema-export.d.ts +9 -0
  115. package/dist/registry/schema-export.d.ts.map +1 -0
  116. package/dist/registry/schema-export.js +132 -0
  117. package/dist/registry/schema-export.js.map +1 -0
  118. package/dist/registry/types.d.ts +29 -0
  119. package/dist/registry/types.d.ts.map +1 -0
  120. package/dist/registry/types.js +5 -0
  121. package/dist/registry/types.js.map +1 -0
  122. package/dist/registry/validation.d.ts +9 -0
  123. package/dist/registry/validation.d.ts.map +1 -0
  124. package/dist/registry/validation.js +33 -0
  125. package/dist/registry/validation.js.map +1 -0
  126. package/dist/schema/annotations.d.ts +8 -0
  127. package/dist/schema/annotations.d.ts.map +1 -0
  128. package/dist/schema/annotations.js +52 -0
  129. package/dist/schema/annotations.js.map +1 -0
  130. package/dist/schema/exporter.d.ts +13 -0
  131. package/dist/schema/exporter.d.ts.map +1 -0
  132. package/dist/schema/exporter.js +71 -0
  133. package/dist/schema/exporter.js.map +1 -0
  134. package/dist/schema/index.d.ts +9 -0
  135. package/dist/schema/index.d.ts.map +1 -0
  136. package/{src/schema/index.ts → dist/schema/index.js} +1 -7
  137. package/dist/schema/index.js.map +1 -0
  138. package/dist/schema/loader.d.ts +30 -0
  139. package/dist/schema/loader.d.ts.map +1 -0
  140. package/dist/schema/loader.js +260 -0
  141. package/dist/schema/loader.js.map +1 -0
  142. package/dist/schema/ref-resolver.d.ts +19 -0
  143. package/dist/schema/ref-resolver.d.ts.map +1 -0
  144. package/dist/schema/ref-resolver.js +212 -0
  145. package/dist/schema/ref-resolver.js.map +1 -0
  146. package/dist/schema/strict.d.ts +7 -0
  147. package/dist/schema/strict.d.ts.map +1 -0
  148. package/dist/schema/strict.js +127 -0
  149. package/dist/schema/strict.js.map +1 -0
  150. package/dist/schema/types.d.ts +53 -0
  151. package/dist/schema/types.d.ts.map +1 -0
  152. package/dist/schema/types.js +31 -0
  153. package/dist/schema/types.js.map +1 -0
  154. package/dist/schema/validator.d.ts +16 -0
  155. package/dist/schema/validator.d.ts.map +1 -0
  156. package/dist/schema/validator.js +71 -0
  157. package/dist/schema/validator.js.map +1 -0
  158. package/dist/trace-context.d.ts +35 -0
  159. package/dist/trace-context.d.ts.map +1 -0
  160. package/dist/trace-context.js +86 -0
  161. package/dist/trace-context.js.map +1 -0
  162. package/dist/utils/index.d.ts +11 -0
  163. package/dist/utils/index.d.ts.map +1 -0
  164. package/dist/utils/index.js +32 -0
  165. package/dist/utils/index.js.map +1 -0
  166. package/dist/utils/pattern.d.ts +5 -0
  167. package/dist/utils/pattern.d.ts.map +1 -0
  168. package/dist/utils/pattern.js +31 -0
  169. package/dist/utils/pattern.js.map +1 -0
  170. package/package.json +24 -3
  171. package/.claude/settings.local.json +0 -12
  172. package/.github/workflows/ci.yml +0 -39
  173. package/.gitmessage +0 -60
  174. package/.pre-commit-config.yaml +0 -28
  175. package/CHANGELOG.md +0 -214
  176. package/CLAUDE.md +0 -68
  177. package/apcore-logo.svg +0 -79
  178. package/planning/acl-system/overview.md +0 -54
  179. package/planning/acl-system/plan.md +0 -92
  180. package/planning/acl-system/state.json +0 -76
  181. package/planning/acl-system/tasks/acl-core.md +0 -226
  182. package/planning/acl-system/tasks/acl-rule.md +0 -92
  183. package/planning/acl-system/tasks/conditional-rules.md +0 -259
  184. package/planning/acl-system/tasks/pattern-matching.md +0 -152
  185. package/planning/acl-system/tasks/yaml-loading.md +0 -271
  186. package/planning/core-executor/overview.md +0 -53
  187. package/planning/core-executor/plan.md +0 -88
  188. package/planning/core-executor/state.json +0 -76
  189. package/planning/core-executor/tasks/async-support.md +0 -106
  190. package/planning/core-executor/tasks/execution-pipeline.md +0 -113
  191. package/planning/core-executor/tasks/redaction.md +0 -85
  192. package/planning/core-executor/tasks/safety-checks.md +0 -65
  193. package/planning/core-executor/tasks/setup.md +0 -75
  194. package/planning/decorator-bindings/overview.md +0 -62
  195. package/planning/decorator-bindings/plan.md +0 -104
  196. package/planning/decorator-bindings/state.json +0 -87
  197. package/planning/decorator-bindings/tasks/binding-directory.md +0 -79
  198. package/planning/decorator-bindings/tasks/binding-loader.md +0 -148
  199. package/planning/decorator-bindings/tasks/explicit-schemas.md +0 -85
  200. package/planning/decorator-bindings/tasks/function-module.md +0 -127
  201. package/planning/decorator-bindings/tasks/module-factory.md +0 -89
  202. package/planning/decorator-bindings/tasks/schema-modes.md +0 -142
  203. package/planning/middleware-system/overview.md +0 -48
  204. package/planning/middleware-system/plan.md +0 -102
  205. package/planning/middleware-system/state.json +0 -65
  206. package/planning/middleware-system/tasks/adapters.md +0 -170
  207. package/planning/middleware-system/tasks/base.md +0 -115
  208. package/planning/middleware-system/tasks/logging-middleware.md +0 -304
  209. package/planning/middleware-system/tasks/manager.md +0 -313
  210. package/planning/observability/overview.md +0 -53
  211. package/planning/observability/plan.md +0 -119
  212. package/planning/observability/state.json +0 -98
  213. package/planning/observability/tasks/context-logger.md +0 -201
  214. package/planning/observability/tasks/exporters.md +0 -121
  215. package/planning/observability/tasks/metrics-collector.md +0 -162
  216. package/planning/observability/tasks/metrics-middleware.md +0 -141
  217. package/planning/observability/tasks/obs-logging-middleware.md +0 -179
  218. package/planning/observability/tasks/span-model.md +0 -120
  219. package/planning/observability/tasks/tracing-middleware.md +0 -179
  220. package/planning/overview.md +0 -81
  221. package/planning/registry-system/overview.md +0 -57
  222. package/planning/registry-system/plan.md +0 -114
  223. package/planning/registry-system/state.json +0 -109
  224. package/planning/registry-system/tasks/dependencies.md +0 -157
  225. package/planning/registry-system/tasks/entry-point.md +0 -148
  226. package/planning/registry-system/tasks/metadata.md +0 -198
  227. package/planning/registry-system/tasks/registry-core.md +0 -323
  228. package/planning/registry-system/tasks/scanner.md +0 -172
  229. package/planning/registry-system/tasks/schema-export.md +0 -261
  230. package/planning/registry-system/tasks/types.md +0 -124
  231. package/planning/registry-system/tasks/validation.md +0 -177
  232. package/planning/schema-system/overview.md +0 -56
  233. package/planning/schema-system/plan.md +0 -121
  234. package/planning/schema-system/state.json +0 -98
  235. package/planning/schema-system/tasks/exporter.md +0 -153
  236. package/planning/schema-system/tasks/loader.md +0 -106
  237. package/planning/schema-system/tasks/ref-resolver.md +0 -133
  238. package/planning/schema-system/tasks/strict-mode.md +0 -140
  239. package/planning/schema-system/tasks/typebox-generation.md +0 -133
  240. package/planning/schema-system/tasks/types-and-annotations.md +0 -160
  241. package/planning/schema-system/tasks/validator.md +0 -149
  242. package/src/acl.ts +0 -200
  243. package/src/async-task.ts +0 -267
  244. package/src/bindings.ts +0 -207
  245. package/src/cancel.ts +0 -32
  246. package/src/config.ts +0 -24
  247. package/src/context.ts +0 -160
  248. package/src/decorator.ts +0 -110
  249. package/src/errors.ts +0 -429
  250. package/src/executor.ts +0 -493
  251. package/src/extensions.ts +0 -265
  252. package/src/middleware/adapters.ts +0 -54
  253. package/src/middleware/base.ts +0 -33
  254. package/src/middleware/logging.ts +0 -103
  255. package/src/middleware/manager.ts +0 -105
  256. package/src/module.ts +0 -43
  257. package/src/observability/context-logger.ts +0 -203
  258. package/src/observability/metrics.ts +0 -214
  259. package/src/observability/tracing.ts +0 -252
  260. package/src/registry/dependencies.ts +0 -99
  261. package/src/registry/entry-point.ts +0 -64
  262. package/src/registry/metadata.ts +0 -111
  263. package/src/registry/registry.ts +0 -580
  264. package/src/registry/scanner.ts +0 -168
  265. package/src/registry/schema-export.ts +0 -181
  266. package/src/registry/types.ts +0 -32
  267. package/src/registry/validation.ts +0 -38
  268. package/src/schema/annotations.ts +0 -68
  269. package/src/schema/exporter.ts +0 -90
  270. package/src/schema/loader.ts +0 -273
  271. package/src/schema/ref-resolver.ts +0 -244
  272. package/src/schema/strict.ts +0 -136
  273. package/src/schema/types.ts +0 -73
  274. package/src/schema/validator.ts +0 -82
  275. package/src/trace-context.ts +0 -102
  276. package/src/utils/index.ts +0 -5
  277. package/src/utils/pattern.ts +0 -30
  278. package/tests/async-task.test.ts +0 -335
  279. package/tests/helpers.ts +0 -30
  280. package/tests/integration/test-acl-safety.test.ts +0 -269
  281. package/tests/integration/test-binding-executor.test.ts +0 -194
  282. package/tests/integration/test-e2e-flow.test.ts +0 -117
  283. package/tests/integration/test-error-propagation.test.ts +0 -259
  284. package/tests/integration/test-middleware-chain.test.ts +0 -120
  285. package/tests/integration/test-observability-integration.test.ts +0 -438
  286. package/tests/observability/test-context-logger.test.ts +0 -123
  287. package/tests/observability/test-metrics.test.ts +0 -186
  288. package/tests/observability/test-tracing.test.ts +0 -303
  289. package/tests/registry/test-dependencies.test.ts +0 -70
  290. package/tests/registry/test-entry-point.test.ts +0 -133
  291. package/tests/registry/test-metadata.test.ts +0 -265
  292. package/tests/registry/test-registry.test.ts +0 -1397
  293. package/tests/registry/test-scanner.test.ts +0 -257
  294. package/tests/registry/test-schema-export.test.ts +0 -355
  295. package/tests/registry/test-validation.test.ts +0 -75
  296. package/tests/schema/test-annotations.test.ts +0 -137
  297. package/tests/schema/test-exporter.test.ts +0 -172
  298. package/tests/schema/test-loader.test.ts +0 -461
  299. package/tests/schema/test-ref-resolver.test.ts +0 -530
  300. package/tests/schema/test-strict.test.ts +0 -348
  301. package/tests/schema/test-validator.test.ts +0 -64
  302. package/tests/test-acl.test.ts +0 -423
  303. package/tests/test-bindings.test.ts +0 -227
  304. package/tests/test-cancel.test.ts +0 -71
  305. package/tests/test-config.test.ts +0 -76
  306. package/tests/test-context.test.ts +0 -266
  307. package/tests/test-decorator.test.ts +0 -173
  308. package/tests/test-errors.test.ts +0 -647
  309. package/tests/test-executor-stream.test.ts +0 -208
  310. package/tests/test-executor.test.ts +0 -252
  311. package/tests/test-extensions.test.ts +0 -310
  312. package/tests/test-logging-middleware.test.ts +0 -150
  313. package/tests/test-middleware-manager.test.ts +0 -185
  314. package/tests/test-middleware.test.ts +0 -86
  315. package/tests/test-trace-context.test.ts +0 -251
  316. package/tests/utils/test-pattern.test.ts +0 -109
  317. package/tsconfig.build.json +0 -8
  318. package/tsconfig.json +0 -20
  319. package/vitest.config.ts +0 -18
@@ -1,162 +0,0 @@
1
- # Task: MetricsCollector with Counters, Histograms, and Prometheus Export
2
-
3
- ## Goal
4
-
5
- Implement `MetricsCollector` that provides in-memory counter and histogram metric primitives with string-encoded composite keys, configurable histogram buckets, and Prometheus text format export. Includes convenience methods for apcore-standard metrics (calls, errors, duration).
6
-
7
- ## Files Involved
8
-
9
- - `src/observability/metrics.ts` -- MetricsCollector class, labelsKey(), parseLabels(), formatLabels() helpers
10
- - `tests/observability/test-metrics.test.ts` -- Unit tests for MetricsCollector
11
-
12
- ## Steps (TDD)
13
-
14
- ### 1. Write failing tests for counter operations
15
-
16
- ```typescript
17
- describe('MetricsCollector', () => {
18
- it('increments counters', () => {
19
- const collector = new MetricsCollector();
20
- collector.increment('test_counter', { method: 'GET' });
21
- collector.increment('test_counter', { method: 'GET' });
22
- const snap = collector.snapshot();
23
- expect(snap.counters['test_counter|method=GET']).toBe(2);
24
- });
25
-
26
- it('supports custom increment amounts', () => {
27
- const collector = new MetricsCollector();
28
- collector.increment('test_counter', { method: 'POST' }, 5);
29
- const snap = collector.snapshot();
30
- expect(snap.counters['test_counter|method=POST']).toBe(5);
31
- });
32
- });
33
- ```
34
-
35
- ### 2. Implement string-encoded key system
36
-
37
- Labels are encoded as sorted key-value pairs joined by commas:
38
-
39
- ```typescript
40
- function labelsKey(labels: Record<string, string>): string {
41
- return Object.entries(labels)
42
- .sort(([a], [b]) => a.localeCompare(b))
43
- .map(([k, v]) => `${k}=${v}`)
44
- .join(',');
45
- }
46
- ```
47
-
48
- Composite keys use the format `name|key1=val1,key2=val2`. Sorting ensures deterministic keys regardless of insertion order. No thread locking is needed because Node.js is single-threaded (unlike the Python implementation which requires locks).
49
-
50
- ### 3. Implement counter increment
51
-
52
- ```typescript
53
- increment(name: string, labels: Record<string, string>, amount: number = 1): void {
54
- const key = `${name}|${labelsKey(labels)}`;
55
- this._counters.set(key, (this._counters.get(key) ?? 0) + amount);
56
- }
57
- ```
58
-
59
- ### 4. Write failing tests for histogram operations
60
-
61
- ```typescript
62
- it('observes histogram values', () => {
63
- const collector = new MetricsCollector();
64
- collector.observe('test_hist', { module: 'a' }, 0.05);
65
- collector.observe('test_hist', { module: 'a' }, 0.5);
66
- const snap = collector.snapshot();
67
- expect(snap.histograms.sums['test_hist|module=a']).toBeCloseTo(0.55);
68
- expect(snap.histograms.counts['test_hist|module=a']).toBe(2);
69
- });
70
- ```
71
-
72
- ### 5. Implement histogram observe with bucket tracking
73
-
74
- ```typescript
75
- observe(name: string, labels: Record<string, string>, value: number): void {
76
- const lk = labelsKey(labels);
77
- const key = `${name}|${lk}`;
78
- this._histogramSums.set(key, (this._histogramSums.get(key) ?? 0) + value);
79
- this._histogramCounts.set(key, (this._histogramCounts.get(key) ?? 0) + 1);
80
-
81
- for (const b of this._buckets) {
82
- if (value <= b) {
83
- const bkey = `${name}|${lk}|${b}`;
84
- this._histogramBuckets.set(bkey, (this._histogramBuckets.get(bkey) ?? 0) + 1);
85
- }
86
- }
87
- const infKey = `${name}|${lk}|Inf`;
88
- this._histogramBuckets.set(infKey, (this._histogramBuckets.get(infKey) ?? 0) + 1);
89
- }
90
- ```
91
-
92
- Default buckets follow Prometheus conventions: `[0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0]`.
93
-
94
- ### 6. Write failing tests for Prometheus export
95
-
96
- ```typescript
97
- it('exportPrometheus produces valid format', () => {
98
- const collector = new MetricsCollector();
99
- collector.incrementCalls('mod.a', 'success');
100
- collector.observeDuration('mod.a', 0.05);
101
- const output = collector.exportPrometheus();
102
- expect(output).toContain('# HELP apcore_module_calls_total Total module calls');
103
- expect(output).toContain('# TYPE apcore_module_calls_total counter');
104
- expect(output).toContain('# TYPE apcore_module_duration_seconds histogram');
105
- expect(output).toContain('_bucket{');
106
- expect(output).toContain('le="+Inf"');
107
- expect(output).toContain('_sum{');
108
- expect(output).toContain('_count{');
109
- });
110
-
111
- it('empty collector returns empty prometheus string', () => {
112
- const collector = new MetricsCollector();
113
- expect(collector.exportPrometheus()).toBe('');
114
- });
115
- ```
116
-
117
- ### 7. Implement exportPrometheus()
118
-
119
- Generates Prometheus text format with:
120
- - `# HELP` and `# TYPE` headers (emitted once per metric name)
121
- - Counter lines: `metric_name{label="value"} count`
122
- - Histogram lines: `_bucket{...,le="bound"}`, `_sum{...}`, `_count{...}`
123
- - Labels formatted as `{key="value",le="bound"}` with `le` sorted last
124
-
125
- ### 8. Implement convenience methods and snapshot/reset
126
-
127
- ```typescript
128
- incrementCalls(moduleId: string, status: string): void {
129
- this.increment('apcore_module_calls_total', { module_id: moduleId, status });
130
- }
131
- incrementErrors(moduleId: string, errorCode: string): void {
132
- this.increment('apcore_module_errors_total', { module_id: moduleId, error_code: errorCode });
133
- }
134
- observeDuration(moduleId: string, durationSeconds: number): void {
135
- this.observe('apcore_module_duration_seconds', { module_id: moduleId }, durationSeconds);
136
- }
137
- ```
138
-
139
- ### 9. Run tests and verify all pass
140
-
141
- ## Acceptance Criteria
142
-
143
- - [x] `MetricsCollector` stores counters in `Map<string, number>` with string-encoded composite keys
144
- - [x] `increment()` supports custom amounts (default 1)
145
- - [x] `observe()` tracks histogram sums, counts, and per-bucket cumulative counts
146
- - [x] Default histogram buckets match Prometheus standard (13 values + Inf)
147
- - [x] Custom bucket arrays accepted and sorted on construction
148
- - [x] `exportPrometheus()` produces valid Prometheus text format with HELP, TYPE, buckets, sum, count
149
- - [x] Labels sorted alphabetically with `le` last in histogram bucket lines
150
- - [x] `snapshot()` returns counters and histograms as plain objects
151
- - [x] `reset()` clears all internal state
152
- - [x] Convenience methods `incrementCalls()`, `incrementErrors()`, `observeDuration()` use apcore-standard metric names
153
- - [x] No thread locking (Node.js single-threaded -- differs from Python implementation)
154
- - [x] All tests pass with `vitest`
155
-
156
- ## Dependencies
157
-
158
- - None (independent pillar)
159
-
160
- ## Estimated Time
161
-
162
- 3 hours
@@ -1,141 +0,0 @@
1
- # Task: MetricsMiddleware with Stack-Based Timing
2
-
3
- ## Goal
4
-
5
- Implement `MetricsMiddleware` that extends the `Middleware` base class to automatically record call counts, error counts, and execution duration for every module call. Uses a stack-based timing approach via `performance.now()` stored in `context.data` to correctly handle nested module-to-module calls.
6
-
7
- ## Files Involved
8
-
9
- - `src/observability/metrics.ts` -- MetricsMiddleware class
10
- - `src/middleware/base.ts` -- Middleware base class (dependency)
11
- - `src/context.ts` -- Context class with shared `data` record (dependency)
12
- - `src/errors.ts` -- ModuleError for error code extraction (dependency)
13
- - `tests/observability/test-metrics.test.ts` -- Unit tests for MetricsMiddleware
14
-
15
- ## Steps (TDD)
16
-
17
- ### 1. Write failing tests for success path
18
-
19
- ```typescript
20
- describe('MetricsMiddleware', () => {
21
- it('records call metrics on success', () => {
22
- const collector = new MetricsCollector();
23
- const mw = new MetricsMiddleware(collector);
24
- const ctx = Context.create();
25
-
26
- mw.before('mod.a', {}, ctx);
27
- mw.after('mod.a', {}, { result: 'ok' }, ctx);
28
-
29
- const snap = collector.snapshot();
30
- const counters = snap.counters as Record<string, number>;
31
- expect(counters['apcore_module_calls_total|module_id=mod.a,status=success']).toBe(1);
32
- });
33
- });
34
- ```
35
-
36
- ### 2. Write failing tests for error path
37
-
38
- ```typescript
39
- it('records error metrics on failure', () => {
40
- const collector = new MetricsCollector();
41
- const mw = new MetricsMiddleware(collector);
42
- const ctx = Context.create();
43
-
44
- mw.before('mod.a', {}, ctx);
45
- mw.onError('mod.a', {}, new Error('boom'), ctx);
46
-
47
- const snap = collector.snapshot();
48
- const counters = snap.counters as Record<string, number>;
49
- expect(counters['apcore_module_calls_total|module_id=mod.a,status=error']).toBe(1);
50
- expect(counters['apcore_module_errors_total|error_code=Error,module_id=mod.a']).toBe(1);
51
- });
52
- ```
53
-
54
- ### 3. Implement before() with stack-based timing
55
-
56
- ```typescript
57
- export class MetricsMiddleware extends Middleware {
58
- private _collector: MetricsCollector;
59
-
60
- constructor(collector: MetricsCollector) {
61
- super();
62
- this._collector = collector;
63
- }
64
-
65
- override before(
66
- _moduleId: string,
67
- _inputs: Record<string, unknown>,
68
- context: Context,
69
- ): null {
70
- const starts = (context.data['_metrics_starts'] as number[]) ?? [];
71
- starts.push(performance.now());
72
- context.data['_metrics_starts'] = starts;
73
- return null;
74
- }
75
- }
76
- ```
77
-
78
- The timing stack at `context.data['_metrics_starts']` is an array of `performance.now()` values. Each `before()` pushes the current time, and the corresponding `after()`/`onError()` pops it. This stack-based approach correctly pairs start/end times even for nested calls (A calls B calls C).
79
-
80
- ### 4. Implement after() with duration conversion
81
-
82
- ```typescript
83
- override after(
84
- moduleId: string,
85
- _inputs: Record<string, unknown>,
86
- _output: Record<string, unknown>,
87
- context: Context,
88
- ): null {
89
- const starts = context.data['_metrics_starts'] as number[];
90
- const startTime = starts.pop()!;
91
- const durationS = (performance.now() - startTime) / 1000;
92
- this._collector.incrementCalls(moduleId, 'success');
93
- this._collector.observeDuration(moduleId, durationS);
94
- return null;
95
- }
96
- ```
97
-
98
- Key detail: `performance.now()` returns monotonic milliseconds, but Prometheus conventions use seconds. The division by 1000 converts to seconds. This differs from Python's `time.time()` which returns wall-clock seconds directly.
99
-
100
- ### 5. Implement onError() with error code extraction
101
-
102
- ```typescript
103
- override onError(
104
- moduleId: string,
105
- _inputs: Record<string, unknown>,
106
- error: Error,
107
- context: Context,
108
- ): null {
109
- const starts = context.data['_metrics_starts'] as number[];
110
- const startTime = starts.pop()!;
111
- const durationS = (performance.now() - startTime) / 1000;
112
- const errorCode = error instanceof ModuleError ? error.code : error.constructor.name;
113
- this._collector.incrementCalls(moduleId, 'error');
114
- this._collector.incrementErrors(moduleId, errorCode);
115
- this._collector.observeDuration(moduleId, durationS);
116
- return null;
117
- }
118
- ```
119
-
120
- Error code is extracted from `ModuleError.code` (structured apcore errors) or falls back to `error.constructor.name` (generic JS errors).
121
-
122
- ### 6. Run tests and verify all pass
123
-
124
- ## Acceptance Criteria
125
-
126
- - [x] `MetricsMiddleware` extends `Middleware` and accepts a `MetricsCollector` instance
127
- - [x] `before()` pushes `performance.now()` onto `context.data['_metrics_starts']` stack
128
- - [x] `after()` pops start time, computes duration in seconds, records success call + duration
129
- - [x] `onError()` pops start time, computes duration, records error call + error count + duration
130
- - [x] Duration converted from milliseconds to seconds (`/ 1000`) for Prometheus conventions
131
- - [x] Error code extracted from `ModuleError.code` or `error.constructor.name`
132
- - [x] Stack-based timing correctly handles nested module calls
133
- - [x] All tests pass with `vitest`
134
-
135
- ## Dependencies
136
-
137
- - **metrics-collector** -- Requires `MetricsCollector` class
138
-
139
- ## Estimated Time
140
-
141
- 2 hours
@@ -1,179 +0,0 @@
1
- # Task: ObsLoggingMiddleware with Stack-Based Timing
2
-
3
- ## Goal
4
-
5
- Implement `ObsLoggingMiddleware` that extends the `Middleware` base class to provide structured logging of module call lifecycle events (start, complete, error) with stack-based `performance.now()` timing and configurable input/output logging. Delegates all log output to a `ContextLogger` instance.
6
-
7
- ## Files Involved
8
-
9
- - `src/observability/context-logger.ts` -- ObsLoggingMiddleware class
10
- - `src/middleware/base.ts` -- Middleware base class (dependency)
11
- - `src/context.ts` -- Context class with shared `data` record (dependency)
12
- - `tests/observability/test-context-logger.test.ts` -- Unit tests for ObsLoggingMiddleware
13
-
14
- ## Steps (TDD)
15
-
16
- ### 1. Write failing tests for before/after lifecycle
17
-
18
- ```typescript
19
- describe('ObsLoggingMiddleware', () => {
20
- it('logs before and after', () => {
21
- const { output, lines } = createBufferOutput();
22
- const logger = new ContextLogger({ output });
23
- const mw = new ObsLoggingMiddleware({ logger });
24
- const ctx = Context.create();
25
-
26
- mw.before('mod.a', { name: 'Alice' }, ctx);
27
- mw.after('mod.a', { name: 'Alice' }, { result: 'ok' }, ctx);
28
-
29
- expect(lines).toHaveLength(2);
30
- const before = JSON.parse(lines[0]);
31
- const after = JSON.parse(lines[1]);
32
- expect(before.message).toBe('Module call started');
33
- expect(before.extra.module_id).toBe('mod.a');
34
- expect(after.message).toBe('Module call completed');
35
- expect(after.extra.duration_ms).toBeDefined();
36
- });
37
- });
38
- ```
39
-
40
- ### 2. Write failing tests for error lifecycle
41
-
42
- ```typescript
43
- it('logs onError', () => {
44
- const { output, lines } = createBufferOutput();
45
- const logger = new ContextLogger({ output });
46
- const mw = new ObsLoggingMiddleware({ logger });
47
- const ctx = Context.create();
48
-
49
- mw.before('mod.a', {}, ctx);
50
- mw.onError('mod.a', {}, new Error('boom'), ctx);
51
-
52
- expect(lines).toHaveLength(2);
53
- const errorLog = JSON.parse(lines[1]);
54
- expect(errorLog.message).toBe('Module call failed');
55
- expect(errorLog.extra.error_type).toBe('Error');
56
- expect(errorLog.extra.duration_ms).toBeDefined();
57
- });
58
- ```
59
-
60
- ### 3. Implement constructor with configurable options
61
-
62
- ```typescript
63
- export class ObsLoggingMiddleware extends Middleware {
64
- private _logger: ContextLogger;
65
- private _logInputs: boolean;
66
- private _logOutputs: boolean;
67
-
68
- constructor(options?: {
69
- logger?: ContextLogger;
70
- logInputs?: boolean; // default: true
71
- logOutputs?: boolean; // default: true
72
- }) {
73
- super();
74
- this._logger = options?.logger ?? new ContextLogger({ name: 'apcore.obs_logging' });
75
- this._logInputs = options?.logInputs ?? true;
76
- this._logOutputs = options?.logOutputs ?? true;
77
- }
78
- }
79
- ```
80
-
81
- ### 4. Implement before() with stack-based timing and start log
82
-
83
- ```typescript
84
- override before(
85
- moduleId: string,
86
- inputs: Record<string, unknown>,
87
- context: Context,
88
- ): null {
89
- const starts = (context.data['_obs_logging_starts'] as number[]) ?? [];
90
- starts.push(performance.now());
91
- context.data['_obs_logging_starts'] = starts;
92
-
93
- const extra: Record<string, unknown> = {
94
- module_id: moduleId,
95
- caller_id: context.callerId,
96
- };
97
- if (this._logInputs) {
98
- extra['inputs'] = context.redactedInputs ?? inputs;
99
- }
100
- this._logger.info('Module call started', extra);
101
- return null;
102
- }
103
- ```
104
-
105
- Note: When `logInputs` is true, the middleware prefers `context.redactedInputs` (already sanitized by the executor's redaction step) over raw inputs.
106
-
107
- ### 5. Implement after() with completion log and duration
108
-
109
- ```typescript
110
- override after(
111
- moduleId: string,
112
- _inputs: Record<string, unknown>,
113
- output: Record<string, unknown>,
114
- context: Context,
115
- ): null {
116
- const starts = context.data['_obs_logging_starts'] as number[];
117
- const startTime = starts.pop()!;
118
- const durationMs = performance.now() - startTime;
119
-
120
- const extra: Record<string, unknown> = {
121
- module_id: moduleId,
122
- duration_ms: durationMs,
123
- };
124
- if (this._logOutputs) {
125
- extra['output'] = output;
126
- }
127
- this._logger.info('Module call completed', extra);
128
- return null;
129
- }
130
- ```
131
-
132
- Duration is kept in milliseconds (not converted to seconds like MetricsMiddleware) since this is for human-readable log output, not Prometheus metrics.
133
-
134
- ### 6. Implement onError() with error details
135
-
136
- ```typescript
137
- override onError(
138
- moduleId: string,
139
- _inputs: Record<string, unknown>,
140
- error: Error,
141
- context: Context,
142
- ): null {
143
- const starts = context.data['_obs_logging_starts'] as number[];
144
- const startTime = starts.pop()!;
145
- const durationMs = performance.now() - startTime;
146
-
147
- this._logger.error('Module call failed', {
148
- module_id: moduleId,
149
- duration_ms: durationMs,
150
- error_type: error.constructor.name,
151
- error_message: String(error),
152
- });
153
- return null;
154
- }
155
- ```
156
-
157
- ### 7. Run tests and verify all pass
158
-
159
- ## Acceptance Criteria
160
-
161
- - [x] `ObsLoggingMiddleware` extends `Middleware` and accepts optional `logger`, `logInputs`, `logOutputs`
162
- - [x] Defaults to a `ContextLogger` named `apcore.obs_logging` if no logger provided
163
- - [x] `before()` pushes `performance.now()` onto `context.data['_obs_logging_starts']` stack
164
- - [x] `before()` emits "Module call started" at info level with module_id and caller_id
165
- - [x] `before()` includes `context.redactedInputs` (or raw inputs) when `logInputs` is true
166
- - [x] `after()` pops start time, computes duration in milliseconds, emits "Module call completed"
167
- - [x] `after()` includes output when `logOutputs` is true
168
- - [x] `onError()` pops start time, computes duration, emits "Module call failed" at error level
169
- - [x] Error log includes `error_type` (constructor name) and `error_message` (stringified error)
170
- - [x] Stack-based timing correctly handles nested module calls
171
- - [x] All tests pass with `vitest`
172
-
173
- ## Dependencies
174
-
175
- - **context-logger** -- Requires `ContextLogger` class
176
-
177
- ## Estimated Time
178
-
179
- 2 hours
@@ -1,120 +0,0 @@
1
- # Task: Span Interface and createSpan() Factory
2
-
3
- ## Goal
4
-
5
- Define the `Span` interface representing a single trace span and implement the `createSpan()` factory function that produces span instances with auto-generated span IDs. Also define the `SpanExporter` interface that all exporter implementations must satisfy.
6
-
7
- ## Files Involved
8
-
9
- - `src/observability/tracing.ts` -- Span interface, createSpan() factory, SpanExporter interface
10
- - `tests/observability/test-tracing.test.ts` -- Unit tests for span creation
11
-
12
- ## Steps (TDD)
13
-
14
- ### 1. Write failing tests for Span creation
15
-
16
- ```typescript
17
- import { describe, it, expect } from 'vitest';
18
- import { createSpan } from '../../src/observability/tracing.js';
19
-
20
- describe('Span', () => {
21
- it('createSpan creates span with defaults', () => {
22
- const span = createSpan({
23
- traceId: 'trace-1',
24
- name: 'test.span',
25
- startTime: 100,
26
- });
27
- expect(span.traceId).toBe('trace-1');
28
- expect(span.name).toBe('test.span');
29
- expect(span.startTime).toBe(100);
30
- expect(span.spanId).toBeDefined();
31
- expect(span.spanId).toHaveLength(16); // randomBytes(8).toString('hex')
32
- expect(span.parentSpanId).toBeNull();
33
- expect(span.status).toBe('ok');
34
- expect(span.endTime).toBeNull();
35
- expect(span.events).toEqual([]);
36
- expect(span.attributes).toEqual({});
37
- });
38
-
39
- it('createSpan accepts custom spanId and parentSpanId', () => {
40
- const span = createSpan({
41
- traceId: 'trace-2',
42
- name: 'child.span',
43
- startTime: 200,
44
- spanId: 'custom-span-id-01',
45
- parentSpanId: 'parent-span-0001',
46
- attributes: { moduleId: 'mod.a' },
47
- });
48
- expect(span.spanId).toBe('custom-span-id-01');
49
- expect(span.parentSpanId).toBe('parent-span-0001');
50
- expect(span.attributes['moduleId']).toBe('mod.a');
51
- });
52
- });
53
- ```
54
-
55
- ### 2. Define the Span interface
56
-
57
- Define a TypeScript `interface Span` with the following fields:
58
- - `traceId: string` -- trace identifier linking related spans
59
- - `name: string` -- operation name (e.g., `apcore.module.execute`)
60
- - `startTime: number` -- `performance.now()` timestamp in milliseconds
61
- - `spanId: string` -- unique 16-hex-char identifier
62
- - `parentSpanId: string | null` -- parent span for nesting, null for root spans
63
- - `attributes: Record<string, unknown>` -- key-value metadata
64
- - `endTime: number | null` -- set when span completes, null while active
65
- - `status: string` -- `'ok'` or `'error'`
66
- - `events: Array<Record<string, unknown>>` -- timeline annotations
67
-
68
- ### 3. Implement createSpan() factory
69
-
70
- ```typescript
71
- import { randomBytes } from 'node:crypto';
72
-
73
- export function createSpan(options: {
74
- traceId: string;
75
- name: string;
76
- startTime: number;
77
- spanId?: string;
78
- parentSpanId?: string | null;
79
- attributes?: Record<string, unknown>;
80
- }): Span {
81
- return {
82
- traceId: options.traceId,
83
- name: options.name,
84
- startTime: options.startTime,
85
- spanId: options.spanId ?? randomBytes(8).toString('hex'),
86
- parentSpanId: options.parentSpanId ?? null,
87
- attributes: options.attributes ?? {},
88
- endTime: null,
89
- status: 'ok',
90
- events: [],
91
- };
92
- }
93
- ```
94
-
95
- ### 4. Define the SpanExporter interface
96
-
97
- ```typescript
98
- export interface SpanExporter {
99
- export(span: Span): void;
100
- }
101
- ```
102
-
103
- ### 5. Run tests and verify all pass
104
-
105
- ## Acceptance Criteria
106
-
107
- - [x] `Span` interface defines all 9 fields with correct types
108
- - [x] `createSpan()` generates 16-character hex span IDs via `randomBytes(8).toString('hex')`
109
- - [x] Default values: `parentSpanId: null`, `attributes: {}`, `endTime: null`, `status: 'ok'`, `events: []`
110
- - [x] Custom `spanId`, `parentSpanId`, and `attributes` can be provided via options
111
- - [x] `SpanExporter` interface declares a single `export(span: Span): void` method
112
- - [x] All tests pass with `vitest`
113
-
114
- ## Dependencies
115
-
116
- - None (foundational task)
117
-
118
- ## Estimated Time
119
-
120
- 1 hour