7.7 KiB
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-useClocketc. 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)
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:
python !== undefinedorshort.startsWith("python-")→pythonpeerDependenciesincludes@react-three/fiberorthreeorshort.startsWith("r3f-")→r3fpeerDependenciesincludesreactorshort.startsWith("react-")→reactbrowser !== undefinedorenginesset with neithernodenorbunkeys →browserbin !== undefinedordependencies/devDependencieshit a known server-only dep (bun:sqlite,pg,ioredis,better-sqlite3,node:*namespace) →server- 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)returnsWorkspaceGraph. Only edges where the target is also innodesare 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 throwsOVERVIEW.CYCLE_DETECTEDcarrying the cycle nodes.detectCycles(graph)returnsstring[][]of strongly-connected components larger than one node (Tarjan).transitiveDependencies(graph, pkg)/transitiveDependents(graph, pkg)BFS in the respective direction. Returns a sortedstring[].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 sortedPluginSummary[].byCategory(records)returnsRecord<string, number>(package count).byTier(records)returnsRecord<Tier, PackageRecord[]>.
8. Error codes
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 perdiscoverPackagescall byrecords.length.overview.records_rejected_total{reason}— counter (reason ∈ {parse_failed,invalid_record}).overview.graph_builds_total— counter incremented perbuildGraph.overview.discover_duration_ms— histogram (boundaries 1, 5, 25, 100, 500, 2500) per successfuldiscoverPackages.
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 andpythonearly-out;tierHintsextension.discover/discover.test.ts— discovers a temp-dir fixture (real files, realpackage.jsons, no mocks — rule 2); async variant honoursAbortSignal; invalid JSON triggersPARSE_FAILED; missing field triggersINVALID_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 afterdiscoverPackages+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.tspushes 500, split intograph.spec.basics.test.ts+graph.spec.cycles.test.ts.