197 lines
7.7 KiB
Markdown
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`.
|