Initial Commit
This commit is contained in:
196
openspec/archive/2026-05-25-enterprise-overview/design.md
Normal file
196
openspec/archive/2026-05-25-enterprise-overview/design.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user