Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
# Proposal — Migrate @suaveplan/profiler to genesis
## Why
`@suaveplan/profiler` is the universal-tier runtime performance profiling primitive consumed by downstream observability tooling. The upstream package at `/mnt/s/web/suaveplan/packages/universal/profiler/` reached 93.95% test coverage and lacks the genesis enterprise gates (`Clock`/`TimersLike`/`LoggerLike` injection, `defineCodes` error registry, OTel meter wiring, Zod-validated boundaries, 100/100/100/100 coverage). This change re-lands the package at `packages/devtools/tooling/profiler/` with the full feature surface and the genesis enterprise contract.
## What
Land `@suaveplan/profiler@0.1.0` with the following modules (one per feature folder, 5-files-per-feature):
- `profiler/` — `Profiler` class: `start`/`stop`/`mark`/`measure`/`measureAsync`
- `sampler/` — periodic metric sampler with injectable `Clock`+`TimersLike`
- `memory/` — heap snapshot capture with injectable capture-fn; leak detection
- `bottleneck/` — hot-path detector + budget-violation finder
- `comparison/` — diff two profiles; regression/improvement classifier
- `flamegraph/` — flamegraph JSON tree builder + recursive-call collapse
- `reporter/` — text formatter + percentile statistics
- `otel/` — OTLP-compatible span exporter (HTTP `fetch`)
- `error-codes/` — `PROFILER_CODES` via `defineCodes` + `ProfilerError`
- `telemetry/` — OTel meter for the profiler itself (counter + histogram)
- `types/` — shared `ProfileEntry`/`ProfileMark`/`SamplePoint`/etc
## Source mapping
| Upstream file | Genesis file | Notes |
| --- | --- | --- |
| `src/profiler/profiler.ts` | `src/profiler/profiler.ts` | `Clock`+`TimersLike` injected; Zod options |
| `src/sampler/sampler.ts` | `src/sampler/sampler.ts` | `TimersLike.setTimer` replaces `setInterval` |
| `src/memory/memory-profiler.ts` | `src/memory/memory.ts` | Injectable `HeapCapture` fn replaces global probe |
| `src/bottleneck/bottleneck-detector.ts` | `src/bottleneck/bottleneck.ts` | Same algorithm, renamed file |
| `src/comparison/profile-comparator.ts` | `src/comparison/comparison.ts` | Same algorithm |
| `src/flamegraph/flamegraph-generator.ts` | `src/flamegraph/flamegraph.ts` | Same algorithm |
| `src/reporter/reporter.ts` | `src/reporter/reporter.ts` | Same algorithm |
| `src/otel/otel-exporter.ts` | `src/otel/otel.ts` | `fetch` is injectable for tests |
| `src/index.ts` | `src/index.ts` | Explicit named re-exports only |
| (none) | `src/error-codes/error-codes.ts` | New — required by genesis rule |
| (none) | `src/telemetry/telemetry.ts` | New — required by genesis rule |
## Deviations from upstream
1. Upstream uses `@suaveplan/events` `EventEmitter` with variadic tuple payloads. Genesis `@suaveplan/events` exposes `EventBus` with single-payload (`emit(type, payload)`) — event maps reshaped to single-object payloads.
2. Upstream calls `Date.now()` and `setInterval` directly. Genesis injects `Clock` + `TimersLike` per the universal-tier contract.
3. Upstream has no error-code registry. Genesis adds `PROFILER_CODES`/`ProfilerError`.
4. Upstream emits no profiler-self-telemetry. Genesis adds a `telemetry/` module emitting per-public-call counters + duration histograms.
5. Boundary validation: every public constructor option object is now Zod-validated.

View File

@@ -0,0 +1,77 @@
# Profiler — Spec Delta
## ADDED Requirements
### Requirement: Profiler captures named measurements with injectable clock
The `Profiler` class SHALL expose `start(label, category?, metadata?)`, `stop(label)`, `mark(label, metadata?)`, `measure(label, fn, category?)`, and `measureAsync(label, fn, category?)`. Wall-clock readings MUST come from an injectable `Clock`; the default MUST be `() => Date.now()` (or `performance.now()` when available — the constructor accepts a `Clock` override). `measure` and `measureAsync` MUST call `stop` even when the wrapped function throws.
**Verified by:** `profiler.test.ts` › "start + stop records an entry with duration", "measure wraps a sync fn and propagates throws", "measureAsync wraps an async fn and propagates throws", "uses the injected clock for timing".
### Requirement: Sampler runs on TimersLike, not setInterval
The `Sampler` class SHALL drive its periodic sampling through an injected `TimersLike.setTimer` and SHALL stop cleanly by calling `TimersLike.clearTimer`. Calling `start()` while already running MUST be a no-op; calling `stop()` while not running MUST be a no-op. Samples MUST be capped at `maxSamples` via FIFO eviction.
**Verified by:** `sampler.test.ts` › "start schedules samples via TimersLike", "stop clears the active timer", "double-start is a no-op", "double-stop is a no-op", "evicts oldest samples beyond maxSamples".
### Requirement: MemoryProfiler accepts injectable HeapCapture
The `MemoryProfiler` SHALL accept an injectable `HeapCapture` function returning `{ heapUsed, heapTotal }`. The default capture function MUST read `process.memoryUsage()` when available, fall back to `performance.memory` (Chrome), and return zeros otherwise. Leak detection SHALL emit `memory:leak-detected` when the last `leakWindowSize` snapshots show monotonic non-decreasing growth ≥ `leakThresholdBytes`.
**Verified by:** `memory.test.ts` › "uses the injected heap-capture", "default capture handles missing process and performance.memory", "detectLeak fires when growth exceeds threshold", "detectLeak ignores stable windows".
### Requirement: PROFILER_CODES registry via defineCodes
The package MUST publish a stable `PROFILER_CODES` registry built with `defineCodes("PROFILER", [...])` and an accompanying `ProfilerError extends SuaveplanError<ProfilerCode>` class with a public constructor delegating to `super`. Every public option-validation failure MUST throw `ProfilerError` with a code from the registry.
**Verified by:** `error-codes.test.ts` › "every code is namespaced under PROFILER.", "ProfilerError carries the supplied code and message", "Profiler throws INVALID_OPTIONS for non-positive percentiles", "Sampler throws INVALID_OPTIONS for non-positive intervalMs".
### Requirement: Telemetry instruments emit on public calls
The package MUST register an OTel meter at `@suaveplan/profiler` exposing:
- `profiler_entries_total` counter — incremented on every completed `stop()`
- `profiler_marks_total` counter — incremented on every `mark()`
- `profiler_measure_duration_ms` histogram — recorded on every completed `stop()`
- `profiler_samples_total` counter — incremented on every `takeSample()`
- `profiler_memory_snapshots_total` counter — incremented on every `takeSnapshot()`
A real in-memory `MeterProvider` registered before module import MUST see metric points after public calls.
**Verified by:** `telemetry.test.ts` › "records entries on stop", "records marks", "records samples", "records snapshots", "records measure duration".
### Requirement: BottleneckDetector ranks hot spots deterministically
`BottleneckDetector.analyze` MUST return up to `maxResults` bottlenecks sorted by `impact = avgDuration * frequency` descending. `findHotSpots` MUST return entries that exceed `timeBudgetMs` and occur ≥ `hotSpotFrequency` times. `findBudgetViolations` MUST return entries whose `duration` exceeds `timeBudgetMs`.
**Verified by:** `bottleneck.test.ts` › "analyze ranks by impact", "findHotSpots requires both threshold + frequency", "findBudgetViolations returns over-budget entries", "respects maxResults cap".
### Requirement: ProfileComparator classifies deltas by regressionThreshold
`ProfileComparator` MUST classify each per-label delta into regressions, improvements, or unchanged based on `percentChange` vs `regressionThreshold`. Regressions MUST be sorted by `percentChange` descending; improvements ascending. Both `compareReports` (ProfileReport pair) and `compareEntries` (entry-array pair) MUST be supported.
**Verified by:** `comparison.test.ts` › "classifies regressions and improvements", "computeReports works at category granularity", "handles zero-baseline edge case", "sorts regressions descending and improvements ascending".
### Requirement: FlameGraphGenerator builds a containment tree
`FlameGraphGenerator.generate(entries)` MUST return a root `FlameNode` whose children are populated by start/end-time containment. `selfTime` MUST equal `value` minus the sum of immediate children's `value`. `collapseRecursive` MUST collapse consecutive same-name parent/child nodes. `toD3Format` MUST emit `{ name, value, children }` JSON.
**Verified by:** `flamegraph.test.ts` › "builds a containment tree", "computes selfTime as value minus children", "collapses recursive nodes", "toD3Format emits d3-compatible JSON".
### Requirement: ProfileReporter computes percentile statistics
`ProfileReporter.generateReport` MUST return `count`, `min`, `max`, `mean`, `median`, configurable `percentiles`, total duration, per-category stats, alerts above `alertThreshold`, and a copy of marks. Empty-input MUST return a zero-stats object.
**Verified by:** `reporter.test.ts` › "computes percentiles", "handles empty entries", "groups by category", "flags alerts above threshold", "formatReport produces stable text output".
### Requirement: OtelExporter emits OTLP-compatible JSON with injectable fetch + rng
`OtelExporter` MUST convert entries to OTLP-compatible spans with `traceId` (16 bytes hex) and `spanId` (8 bytes hex). A `Fetch` function and `Rng` (random-bytes) MUST be injectable. `export(spans)` MUST return `true` on `response.ok`, `false` otherwise (including thrown errors). `exportBatch()` MUST chunk pending spans into `batchSize` slices.
**Verified by:** `otel.test.ts` › "converts entries to OTLP spans", "respects injected fetch", "returns false on non-ok response", "returns false on fetch throw", "exportBatch chunks pending spans".
### Requirement: Sibling .md per implementation .ts
Every non-barrel, non-test implementation `.ts` file MUST have a sibling `.md` file with the six required sections (Purpose, Features, Usage: Basic, Usage: Advanced, API Reference, Implementation Notes), ≥200 words excluding code.
**Verified by:** `bun run docs:check` at the package level (script is wired in `package.json`).

View File

@@ -0,0 +1,55 @@
# Tasks — Migrate @suaveplan/profiler to genesis
## 0. Pre-flight (non-negotiable)
- [x] 0.1 `git status` clean inside `.claude/worktrees/agent-a53dffba816b6eb95`
- [x] 0.2 Account for every worktree returned by `git worktree list` (this agent's worktree + main checkout)
- [x] 0.3 Confirm `MANIFEST.md` package count matches `find packages -maxdepth 3 -name package.json`
- [x] 0.4 §0a Sync: `git merge-base --is-ancestor 56c1fae5078a5afff119847b4b96d86c78668f02 HEAD` reports true after `git fetch /home/paul/web/genesis genesis && git merge FETCH_HEAD --no-edit`
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/enterprise/profiler.md` end-to-end
- [x] 1.2 Read `openspec/AGENTS.md` Sections 13 and 14
- [x] 1.3 Draft `proposal.md` with `## Source mapping` table
- [x] 1.4 Draft `specs/profiler/spec.md` with `## ADDED Requirements` and `**Verified by:**` lines
## 2. Implementation
- [x] 2.1 `src/types/` — shared types module
- [x] 2.2 `src/error-codes/` — `PROFILER_CODES` registry + `ProfilerError`
- [x] 2.3 `src/telemetry/` — OTel meter, counters, histograms
- [x] 2.4 `src/profiler/` — `Profiler` class with `Clock`-injection
- [x] 2.5 `src/sampler/` — `Sampler` with `TimersLike` + injectable memory-probe
- [x] 2.6 `src/memory/` — `MemoryProfiler` with injectable `HeapCapture`
- [x] 2.7 `src/bottleneck/` — `BottleneckDetector`
- [x] 2.8 `src/comparison/` — `ProfileComparator`
- [x] 2.9 `src/flamegraph/` — `FlameGraphGenerator`
- [x] 2.10 `src/reporter/` — `ProfileReporter`
- [x] 2.11 `src/otel/` — `OtelExporter` with injectable `fetch` + crypto-rng
- [x] 2.12 `src/index.ts` — explicit named re-exports
## 3. Tests
- [x] 3.1 Unit tests at `src/<feature>/<feature>.test.ts` for every module
- [x] 3.2 100/100/100/100 coverage gate met (istanbul)
- [x] 3.3 Every `it(...)` carries `{ timeout: 5000 }`
- [x] 3.4 All tests import from `@suaveplan/testing/runner`
## 4. Documentation
- [x] 4.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced)
- [x] 4.2 `README.md` at package root
## 5. Pre-archive verification
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/profiler` green
- [ ] 5.2 `bunx turbo test:coverage --filter=@suaveplan/profiler` 100/100/100/100
- [ ] 5.3 `bunx turbo build --filter=@suaveplan/profiler` green
- [ ] 5.4 `bun scripts/lint/check-feature-layout.ts | grep profiler` — empty (no violations)
- [ ] 5.5 `bun scripts/folder-reorg/generate-manifest.ts` refreshes MANIFEST.md
## 6. Archive (post-merge — orchestrator's job)
- [ ] 6.1 Fold spec delta into `openspec/specs/profiler/spec.md`
- [ ] 6.2 Move `openspec/changes/profiler/` → `openspec/archive/<date>-profiler/`