Files
suaveplan/openspec/archive/2026-05-25-enterprise-overview/design.md
2026-05-27 23:29:53 +01:00

197 lines
7.7 KiB
Markdown

# Design — `@suaveplan/overview`
> Non-normative design notes for the enterprise-overview change. Normative
> requirements live in `./specs/overview/spec.md`.
---
## 1. What the library is
`@suaveplan/overview` is the **typed, schema-validated, graph-aware library
form of the monorepo manifest**. The data model mirrors what
`scripts/folder-reorg/generate-manifest.ts` produces ad-hoc today (and that
script's own header comment names this package as the eventual replacement).
It is a **leaf** package. Direct workspace deps:
- `@suaveplan/error` — `SuaveplanError`, `defineCodes`
- `@suaveplan/types` — for option-bundle ergonomics (we re-use `Clock` etc. only
if useful; not required by the core)
Plus catalog deps `zod`, `tslib`, `@opentelemetry/api`.
## 2. Tier model
Universal-tier **for pure data + graph operations**. The discovery surface
(`discoverPackages`) touches `node:fs` and `node:path` and is therefore
server-tier; it ships under the `./discover` sub-path export so universal
consumers (browser, edge, R3F) can `import` only the pure pieces. The package
itself stays universal at the root entry — only the `./discover` sub-path is
server-flavoured. This matches the genesis pattern used by `db-sqlite` /
`packer-browser` etc.
## 3. Data model (Zod-validated)
```ts
type Tier = "python" | "r3f" | "react" | "browser" | "server" | "universal";
interface PackageRecord {
name: string; // "@suaveplan/<short>"
short: string; // "<short>"
category: string; // top-level super-category folder
plugin: string; // mid-level plugin folder
dirName: string; // last path segment
version: string;
description: string;
tier: Tier;
workspaceDeps: string[]; // sorted, deduped @suaveplan/* names
hasBin: boolean;
hasBrowserField: boolean;
}
interface PluginSummary {
category: string;
plugin: string;
packages: number;
workspaceDeps: number; // total edges out
}
interface SuavePlanManifest {
packages: PackageRecord[];
plugins: PluginSummary[];
categories: string[];
tierCounts: Record<Tier, number>;
}
interface WorkspaceGraph {
nodes: Set<string>;
forward: Map<string, Set<string>>; // pkg → deps
reverse: Map<string, Set<string>>; // pkg → dependents
}
```
All four shapes have Zod schemas exported alongside the types.
## 4. Tier inference
Identical priority to `generate-manifest.ts`:
1. `python !== undefined` or `short.startsWith("python-")` → `python`
2. `peerDependencies` includes `@react-three/fiber` or `three` or
`short.startsWith("r3f-")` → `r3f`
3. `peerDependencies` includes `react` or `short.startsWith("react-")` →
`react`
4. `browser !== undefined` or `engines` set with neither `node` nor `bun`
keys → `browser`
5. `bin !== undefined` or `dependencies`/`devDependencies` hit a known
server-only dep (`bun:sqlite`, `pg`, `ioredis`, `better-sqlite3`,
`node:*` namespace) → `server`
6. fallback → `universal`
The hint set is exported as `SERVER_DEP_HINTS` so downstream code can extend
it via a `tierHints` option to `discoverPackages`.
## 5. Discovery semantics
`discoverPackages(options)` walks
`<root>/packages/<category>/<plugin>/<pkg>/package.json` exactly three levels
deep. Anything that fails `JSON.parse` is reported via the
`OVERVIEW.PARSE_FAILED` error path (caller decides: throw or skip). Anything
without a `package.json` is silently ignored — that matches the script
behaviour and reflects partial-migration reality on `genesis`.
The walk is **synchronous by default** (cheap on a populated monorepo) with
an optional async generator variant (`discoverPackagesIter`) that yields one
`PackageRecord` at a time and honours `AbortSignal`.
## 6. Graph utilities
- `buildGraph(records)` returns `WorkspaceGraph`. Only edges where the target
is also in `nodes` are recorded (so a missing-yet-to-migrate dep is dropped
rather than producing a phantom node — matches the script's behaviour).
- `topologicalOrder(graph)` returns packages in dep-first order using
Kahn's algorithm. On a cycle it throws `OVERVIEW.CYCLE_DETECTED` carrying
the cycle nodes.
- `detectCycles(graph)` returns `string[][]` of strongly-connected components
larger than one node (Tarjan).
- `transitiveDependencies(graph, pkg)` / `transitiveDependents(graph, pkg)`
BFS in the respective direction. Returns a sorted `string[]`.
- `leafPackages(graph)` — packages with no incoming workspace edge (no
dependents).
- `rootPackages(graph)` — packages with no outgoing workspace edge (no deps).
## 7. Roll-ups
- `byPlugin(records)` returns a sorted `PluginSummary[]`.
- `byCategory(records)` returns `Record<string, number>` (package count).
- `byTier(records)` returns `Record<Tier, PackageRecord[]>`.
## 8. Error codes
```ts
defineCodes("OVERVIEW", [
"PARSE_FAILED", // package.json failed JSON.parse
"INVALID_RECORD", // a record failed Zod validation
"CYCLE_DETECTED", // topological sort hit a cycle
"UNKNOWN_PACKAGE", // queried a package not in the graph
"DISCOVERY_ABORTED", // AbortSignal fired mid-walk
"INVALID_TIER", // schema rejected an out-of-range tier string
] as const);
```
Every code is thrown from a real call site (no dead entries — §2c).
## 9. OpenTelemetry
Three counters and one histogram, namespaced `overview.*`:
- `overview.packages_discovered_total` — counter incremented per
`discoverPackages` call by `records.length`.
- `overview.records_rejected_total{reason}` — counter (reason ∈
{`parse_failed`, `invalid_record`}).
- `overview.graph_builds_total` — counter incremented per `buildGraph`.
- `overview.discover_duration_ms` — histogram (boundaries 1, 5, 25, 100,
500, 2500) per successful `discoverPackages`.
The OTel meter is obtained via `metrics.getMeter("@suaveplan/overview",
"0.1.0")` so a no-op OTel SDK is the default — no instrumentation cost on
the cold path.
## 10. Why a sub-path for discovery
Tier purity: the package root MUST be safe to import from a universal
context (browser, R3F, edge worker). Node-only `node:fs` / `node:path`
imports live under `./discover` which `package.json` `exports` lists
distinctly. This matches the pattern used by `db-sqlite`.
## 11. Test plan
- **`errors/errors.test.ts`** — every code constructable; cause chain
preserved; `instanceof OverviewError`; code uniqueness.
- **`schemas/schemas.test.ts`** — every shape parses a valid example +
rejects each malformed shape (one negative case per field).
- **`tier/tier.test.ts`** — one case per inference rung incl. server-hint
walk and `python` early-out; `tierHints` extension.
- **`discover/discover.test.ts`** — discovers a temp-dir fixture (real
files, real `package.json`s, no mocks — rule 2); async variant honours
`AbortSignal`; invalid JSON triggers `PARSE_FAILED`; missing field triggers
`INVALID_RECORD`.
- **`graph/graph.test.ts`** — adjacency build; topo on linear + DAG; cycle
case throws + reports; transitive closure correctness on a chain; leaves +
roots on a star; missing-target edge is dropped.
- **`rollups/rollups.test.ts`** — plugin / category / tier aggregates on a
hand-crafted record array.
- **`telemetry/telemetry.test.ts`** — installs an in-memory metric reader,
observes counter + histogram values after `discoverPackages` + `buildGraph`.
Property-based tests via `fast-check` for `topologicalOrder` (random DAG →
all edges respected) and `transitiveDependencies` (closure is reflexive on
self-loops and matches multi-step BFS).
## 12. File-size discipline
The largest implementation file is expected to be `graph/graph.ts` (Tarjan
+ topo + closure) at ~180 lines. Everything else stays under 200. Test
files are budget-aware; if `graph.test.ts` pushes 500, split into
`graph.spec.basics.test.ts` + `graph.spec.cycles.test.ts`.