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,106 @@
# @suaveplan/overview - Machine-readable Suaveplan project manifest: typed PackageRecord / SuavePlanManifest / WorkspaceGraph, Zod schemas, tier inference, filesystem discovery (server sub-path), graph utilities (topo, cycle detection, transitive closure, leaves, roots), plugin/category/tier roll-ups, six OVERVIEW.* error codes, three OTel counters + one histogram.
> Machine-readable project manifest + graph utilities for the Suaveplan
> project.
`@suaveplan/overview` turns a populated
`packages/<category>/<plugin>/<pkg>/package.json` tree into a typed,
Zod-validated `SuavePlanManifest` and gives you graph utilities — forward /
reverse adjacency, topological order, cycle detection, transitive closure,
leaves, roots, plugin / category / tier roll-ups — for everything downstream.
It is the **library form** of the data model produced by
`scripts/folder-reorg/generate-manifest.ts` (whose own header comment names this
package as the eventual replacement).
The pure-data surface lives at the root entry. Filesystem-walking code (which
needs `node:fs` / `node:path`) lives behind the `./discover` sub-path so the
root entry stays universal.
<!-- begin:auto-docs -->
## Documentation tree
- [discover](./src/discover/discover.md) — Implements the filesystem walker that produces a validated `SuavePlanManifest` from a populated `packages/<category>/<pl…
- [discover](./src/discover/discover.md) — Implements the filesystem walker that produces a validated `SuavePlanManifest` from a populated `packages/<category>/<pl…
- [errors](./src/errors/errors.md) — Defines the stable error-code registry and the concrete `OverviewError` subclass of `SuaveplanError`. Every public entr…
- [errors](./src/errors/errors.md) — Defines the stable error-code registry and the concrete `OverviewError` subclass of `SuaveplanError`. Every public entr…
- [graph](./src/graph/graph.md) — Implements the graph utilities that operate on a `WorkspaceGraph` built from a list of `PackageRecord`s. Covers the ana…
- [graph](./src/graph/graph.md) — Implements the graph utilities that operate on a `WorkspaceGraph` built from a list of `PackageRecord`s. Covers the ana…
- [rollups](./src/rollups/rollups.md) — Provides the three aggregate views over a `PackageRecord[]` that the manifest needs: plugin roll-ups (one row per `(cat…
- [rollups](./src/rollups/rollups.md) — Provides the three aggregate views over a `PackageRecord[]` that the manifest needs: plugin roll-ups (one row per `(cat…
- [schemas](./src/schemas/schemas.md) — Provides Zod schemas (and one helper, `parseTier`) for every public shape that `@suaveplan/overview` produces or accept…
- [schemas](./src/schemas/schemas.md) — Provides Zod schemas (and one helper, `parseTier`) for every public shape that `@suaveplan/overview` produces or accept…
- [telemetry](./src/telemetry/telemetry.md) — Owns the OpenTelemetry meter for `@suaveplan/overview` and exposes the three counters and one histogram that the spec r…
- [telemetry](./src/telemetry/telemetry.md) — Owns the OpenTelemetry meter for `@suaveplan/overview` and exposes the three counters and one histogram that the spec r…
- [tier](./src/tier/tier.md) — Computes the deterministic `Tier` label for a package given its shortname and the parsed `package.json` shape. The infe…
- [tier](./src/tier/tier.md) — Computes the deterministic `Tier` label for a package given its shortname and the parsed `package.json` shape. The infe…
- [types](./src/types/types.md) — Declares the type-level vocabulary of `@suaveplan/overview`: the closed `Tier` union, the `PackageRecord` row, the `Plu…
- [types](./src/types/types.md) — Declares the type-level vocabulary of `@suaveplan/overview`: the closed `Tier` union, the `PackageRecord` row, the `Plu…
<!-- end:auto-docs -->
## Install
```sh
bun add @suaveplan/overview
```
## Surfaces
| Sub-path | Use |
|---|---|
| `@suaveplan/overview` | Types, schemas, errors, graph utilities, roll-ups, telemetry — all universal |
| `@suaveplan/overview/discover` | `discoverPackages` + `discoverPackagesIter` — server-tier (touches `node:fs`) |
| `@suaveplan/overview/errors` | `OVERVIEW_CODES`, `OverviewError` |
| `@suaveplan/overview/schemas` | Zod schemas + `parseTier` |
| `@suaveplan/overview/tier` | `inferTier`, `SERVER_DEP_HINTS` |
| `@suaveplan/overview/graph` | Graph utilities |
| `@suaveplan/overview/rollups` | `byPlugin`, `byCategory`, `byTier` |
| `@suaveplan/overview/telemetry` | OTel instruments |
## Example
```ts
import { discoverPackages } from "@suaveplan/overview/discover";
import {
buildGraph,
topologicalOrder,
byTier,
} from "@suaveplan/overview";
const manifest = discoverPackages({ root: process.cwd() });
const graph = buildGraph(manifest.packages);
const order = topologicalOrder(graph);
const buckets = byTier(manifest.packages);
console.log(`Packages: ${manifest.packages.length}`);
console.log(`Universal: ${buckets.universal.length}`);
console.log(`Build order (deps first): ${order.slice(0, 5).join(", ")} …`);
```
## Error codes
| Code | Thrown by |
|---|---|
| `OVERVIEW.PARSE_FAILED` | `discoverPackages` — `package.json` failed `JSON.parse` |
| `OVERVIEW.INVALID_RECORD` | `discoverPackages` — row failed `PackageRecordSchema` |
| `OVERVIEW.CYCLE_DETECTED` | `topologicalOrder` |
| `OVERVIEW.UNKNOWN_PACKAGE` | `transitiveDependencies` / `transitiveDependents` |
| `OVERVIEW.DISCOVERY_ABORTED` | `discoverPackagesIter` |
| `OVERVIEW.INVALID_TIER` | `parseTier` |
## Telemetry
Three counters and one histogram, scoped to meter
`@suaveplan/overview@0.1.0`:
- `overview.packages_discovered_total`
- `overview.records_rejected_total{reason}`
- `overview.graph_builds_total`
- `overview.discover_duration_ms`
Plug `@opentelemetry/sdk-metrics` in your runtime to harvest them.
## Tier
Universal at the root entry; server at `./discover`.

View File

@@ -0,0 +1,97 @@
{
"name": "@suaveplan/overview",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Machine-readable Suaveplan project manifest: typed PackageRecord / SuavePlanManifest / WorkspaceGraph, Zod schemas, tier inference, filesystem discovery (server sub-path), graph utilities (topo, cycle detection, transitive closure, leaves, roots), plugin/category/tier roll-ups, six OVERVIEW.* error codes, three OTel counters + one histogram.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./discover": {
"import": {
"types": "./dist/discover/discover.d.ts",
"default": "./dist/discover/discover.js"
}
},
"./errors": {
"import": {
"types": "./dist/errors/errors.d.ts",
"default": "./dist/errors/errors.js"
}
},
"./schemas": {
"import": {
"types": "./dist/schemas/schemas.d.ts",
"default": "./dist/schemas/schemas.js"
}
},
"./tier": {
"import": {
"types": "./dist/tier/tier.d.ts",
"default": "./dist/tier/tier.js"
}
},
"./graph": {
"import": {
"types": "./dist/graph/graph.d.ts",
"default": "./dist/graph/graph.js"
}
},
"./rollups": {
"import": {
"types": "./dist/rollups/rollups.d.ts",
"default": "./dist/rollups/rollups.js"
}
},
"./telemetry": {
"import": {
"types": "./dist/telemetry/telemetry.d.ts",
"default": "./dist/telemetry/telemetry.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/error": "workspace:^",
"@suaveplan/types": "workspace:^",
"@opentelemetry/api": "catalog:observability",
"zod": "catalog:forms",
"tslib": "catalog:build"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"fast-check": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": ["dist", "src", "README.md"]
}

View File

@@ -0,0 +1,85 @@
# `discover/discover.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the filesystem walker that produces a validated `SuavePlanManifest`
from a populated `packages/<category>/<plugin>/<pkg>/package.json` tree. Both
the synchronous `discoverPackages` and the async `discoverPackagesIter` live
here. The module is the only server-tier surface in `@suaveplan/overview`; it
ships under the `./discover` sub-path export so universal consumers can avoid
the `node:fs` / `node:path` imports entirely.
Discovery is straightforward: walk three directory levels deep, read each
`package.json`, infer its tier, collect workspace deps, validate against
`PackageRecordSchema`, then aggregate the resulting rows into a manifest.
## Features
- Synchronous `discoverPackages` returning a fully-validated manifest
- Async `discoverPackagesIter` honouring `AbortSignal`
- `onError: "throw" | "skip"` for graceful handling of malformed
`package.json` files
- Optional `tierHints` extending `SERVER_DEP_HINTS`
- Emits `overview.packages_discovered_total`,
`overview.records_rejected_total{reason}`, and
`overview.discover_duration_ms`
- Built-in roll-up so the manifest carries `plugins`, `categories`, and
`tierCounts` ready for consumption
## Usage: Basic
```ts
import { discoverPackages } from "@suaveplan/overview/discover";
const manifest = discoverPackages({ root: process.cwd() });
console.log(`Found ${manifest.packages.length} packages.`);
```
## Usage: Advanced
```ts
import { discoverPackagesIter } from "@suaveplan/overview/discover";
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
try {
for await (const r of discoverPackagesIter({
root: "/repo",
signal: controller.signal,
onError: "skip",
tierHints: new Set(["custom-native-binding"]),
})) {
process.stdout.write(`${r.name}\n`);
}
} catch (err) {
// OverviewError with code OVERVIEW.DISCOVERY_ABORTED when the signal fired.
}
```
`onError: "skip"` is the right mode for tooling that should never abort on a
single bad package.json (e.g. a long-running watcher); `"throw"` is right for
one-shot CLIs that prefer a hard fail on corruption.
## API Reference
- `discoverPackages(options: DiscoverOptions): SuavePlanManifest`
- `discoverPackagesIter(options: AsyncDiscoverOptions): AsyncGenerator<PackageRecord>`
- `interface DiscoverOptions { root, onError?, tierHints? }`
- `interface AsyncDiscoverOptions extends DiscoverOptions { signal? }`
- `type DiscoveryErrorMode = "throw" | "skip"`
## Implementation Notes
The walker enforces exactly three directory levels (category → plugin →
package). Anything shallower or deeper is silently ignored — partial-migration
trees are common and a permissive walker keeps the tooling robust.
`existsSync` + `statSync` are used to filter non-directories and packages
without a `package.json`; both are cheap on a populated tree because the OS
will have cached the directory listings. The histogram observation uses
`performance.now()` when available (it always is in Node 20+/Bun) and falls
back to `Date.now()` otherwise.

View File

@@ -0,0 +1,405 @@
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
afterEach,
beforeEach,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { OverviewError } from "../errors/errors.js";
import { discoverPackages, discoverPackagesIter } from "./discover.js";
interface PkgFixture {
readonly category: string;
readonly plugin: string;
readonly dir: string;
readonly json: object | string; // string = raw bytes (e.g. malformed)
}
function writeFixture(root: string, pkg: PkgFixture): void {
const dir = join(root, "packages", pkg.category, pkg.plugin, pkg.dir);
mkdirSync(dir, { recursive: true });
const body =
typeof pkg.json === "string" ? pkg.json : JSON.stringify(pkg.json);
writeFileSync(join(dir, "package.json"), body);
}
let tmp: string;
beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), "overview-test-"));
});
afterEach(() => {
// node:fs has no recursive rm in our minimal type set; we rely on tmp dir
// being scrubbed by the OS. Tests don't depend on prior content.
});
describe("discoverPackages", () => {
it("discovers a 3-level fixture", { timeout: 5000 }, () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "a",
json: {
name: "@suaveplan/a",
version: "0.1.0",
description: "alpha",
},
});
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "b",
json: {
name: "@suaveplan/b",
version: "0.1.0",
description: "beta",
dependencies: { "@suaveplan/a": "workspace:^" },
},
});
const manifest = discoverPackages({ root: tmp });
expect(manifest.packages.length).toBe(2);
expect(manifest.packages[0].name).toBe("@suaveplan/a");
expect(manifest.packages[1].workspaceDeps).toEqual(["@suaveplan/a"]);
expect(manifest.categories).toEqual(["foundation"]);
expect(manifest.tierCounts.universal).toBe(2);
expect(manifest.plugins[0]).toEqual({
category: "foundation",
plugin: "core",
packages: 2,
workspaceDeps: 1,
});
});
it("returns an empty manifest for a missing packages/ directory", {
timeout: 5000,
}, () => {
const manifest = discoverPackages({ root: tmp });
expect(manifest.packages.length).toBe(0);
expect(manifest.categories).toEqual([]);
expect(manifest.tierCounts.universal).toBe(0);
});
it("throws PARSE_FAILED on invalid JSON", { timeout: 5000 }, () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "broken",
json: "{ not valid",
});
try {
discoverPackages({ root: tmp });
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.PARSE_FAILED");
expect(err.cause).toBeInstanceOf(Error);
}
}
});
it("throws INVALID_RECORD when fields missing", { timeout: 5000 }, () => {
// No `name` field; schema requires `@suaveplan/...` prefix.
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "x",
json: { version: "0.1.0" },
});
try {
discoverPackages({ root: tmp });
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.INVALID_RECORD");
}
}
});
it("onError: 'skip' drops bad rows and keeps good ones", {
timeout: 5000,
}, () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "good",
json: {
name: "@suaveplan/good",
version: "0.1.0",
description: "g",
},
});
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "broken",
json: "{ not valid",
});
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "invalid",
json: { version: "0.1.0" },
});
const manifest = discoverPackages({ root: tmp, onError: "skip" });
expect(manifest.packages.length).toBe(1);
expect(manifest.packages[0].name).toBe("@suaveplan/good");
});
it("ignores subfolders without package.json + stray files at every level", {
timeout: 5000,
}, () => {
mkdirSync(join(tmp, "packages", "foundation", "core", "empty"), {
recursive: true,
});
// Stray file at category level (not a directory)
mkdirSync(join(tmp, "packages"), { recursive: true });
writeFileSync(join(tmp, "packages", "stray-category.txt"), "stray");
// Stray file at plugin level
mkdirSync(join(tmp, "packages", "foundation"), { recursive: true });
writeFileSync(
join(tmp, "packages", "foundation", "stray-plugin.txt"),
"stray"
);
// Stray file at package level
mkdirSync(join(tmp, "packages", "foundation", "core"), {
recursive: true,
});
writeFileSync(
join(tmp, "packages", "foundation", "core", "stray-pkg.txt"),
"stray"
);
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "x",
json: {
name: "@suaveplan/x",
version: "0.1.0",
description: "",
},
});
const manifest = discoverPackages({ root: tmp });
expect(manifest.packages.length).toBe(1);
});
});
describe("discoverPackagesIter", () => {
it("yields one record at a time", { timeout: 5000 }, async () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "a",
json: {
name: "@suaveplan/a",
version: "0.1.0",
description: "",
},
});
const names: string[] = [];
for await (const r of discoverPackagesIter({ root: tmp })) {
names.push(r.name);
}
expect(names).toEqual(["@suaveplan/a"]);
});
it("honours AbortSignal", { timeout: 5000 }, async () => {
for (const dir of ["a", "b", "c"]) {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir,
json: {
name: `@suaveplan/${dir}`,
version: "0.1.0",
description: "",
},
});
}
const controller = new AbortController();
const iter = discoverPackagesIter({
root: tmp,
signal: controller.signal,
});
const first = await iter.next();
expect(first.done).toBe(false);
controller.abort();
try {
await iter.next();
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.DISCOVERY_ABORTED");
}
}
});
it("returns immediately when packages/ does not exist", {
timeout: 5000,
}, async () => {
const names: string[] = [];
for await (const r of discoverPackagesIter({ root: tmp })) {
names.push(r.name);
}
expect(names).toEqual([]);
});
it("skip mode passes through bad rows", { timeout: 5000 }, async () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "good",
json: {
name: "@suaveplan/good",
version: "0.1.0",
description: "",
},
});
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "broken",
json: "{ not valid",
});
const names: string[] = [];
for await (const r of discoverPackagesIter({
root: tmp,
onError: "skip",
})) {
names.push(r.name);
}
expect(names).toEqual(["@suaveplan/good"]);
});
it("iter throws PARSE_FAILED in throw mode", {
timeout: 5000,
}, async () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "broken",
json: "{ not valid",
});
try {
for await (const _ of discoverPackagesIter({ root: tmp })) {
void _;
}
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.PARSE_FAILED");
}
}
});
it("iter ignores stray files at the category/plugin/pkg level", {
timeout: 5000,
}, async () => {
mkdirSync(join(tmp, "packages"), { recursive: true });
writeFileSync(join(tmp, "packages", "stray.txt"), "x");
mkdirSync(join(tmp, "packages", "foundation"), { recursive: true });
writeFileSync(join(tmp, "packages", "foundation", "stray.txt"), "x");
mkdirSync(join(tmp, "packages", "foundation", "core"), {
recursive: true,
});
writeFileSync(
join(tmp, "packages", "foundation", "core", "stray.txt"),
"x"
);
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "a",
json: {
name: "@suaveplan/a",
version: "0.1.0",
description: "",
},
});
const names: string[] = [];
for await (const r of discoverPackagesIter({ root: tmp })) {
names.push(r.name);
}
expect(names).toEqual(["@suaveplan/a"]);
});
it("skip mode tolerates missing version + description fields", {
timeout: 5000,
}, () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "x",
json: { name: "@suaveplan/x" },
});
const manifest = discoverPackages({ root: tmp, onError: "skip" });
expect(manifest.packages.length).toBe(0);
});
it("collectWorkspaceDeps ignores non-workspace ranges and non-suaveplan names", {
timeout: 5000,
}, () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "x",
json: {
name: "@suaveplan/x",
version: "0.1.0",
description: "",
dependencies: {
"@suaveplan/skip-me": "^1.0.0",
"external-pkg": "workspace:^",
},
optionalDependencies: {
"@suaveplan/opt": "workspace:^",
},
peerDependencies: {
"@suaveplan/peer": "workspace:^",
},
},
});
const manifest = discoverPackages({ root: tmp });
expect(manifest.packages[0]?.workspaceDeps).toEqual([
"@suaveplan/opt",
"@suaveplan/peer",
]);
});
it("iter throws INVALID_RECORD in throw mode", {
timeout: 5000,
}, async () => {
writeFixture(tmp, {
category: "foundation",
plugin: "core",
dir: "x",
json: { version: "0.1.0" },
});
try {
for await (const _ of discoverPackagesIter({ root: tmp })) {
void _;
}
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.INVALID_RECORD");
}
}
});
});
describe("tier purity at the root entry", () => {
it("root barrel imports no node:* modules", { timeout: 5000 }, () => {
const indexPath = join(__dirname, "..", "index.ts");
const text = readFileSync(indexPath, "utf8");
expect(text.includes('from "node:')).toBe(false);
expect(text.includes('require("node:')).toBe(false);
expect(text.includes("./discover/")).toBe(false);
});
});

View File

@@ -0,0 +1,226 @@
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { OVERVIEW_CODES, OverviewError } from "../errors/errors.js";
import { byPlugin } from "../rollups/rollups.js";
import { PackageRecordSchema } from "../schemas/schemas.js";
import {
discoverDurationMs,
packagesDiscoveredTotal,
recordsRejectedTotal,
} from "../telemetry/telemetry.js";
import { inferTier } from "../tier/tier.js";
import type {
SuavePlanManifest,
PackageRecord,
RawPackageJson,
Tier,
} from "../types/types.js";
/**
* What happens when discovery encounters a `package.json` that fails to
* parse as JSON or fails Zod validation.
*
* - `"throw"` (default) — abort discovery and throw an `OverviewError`.
* - `"skip"` — drop the row, emit a `records_rejected_total` counter point,
* and continue.
*/
export type DiscoveryErrorMode = "throw" | "skip";
export interface DiscoverOptions {
/**
* Root path containing a `packages/` folder. Discovery walks
* `<root>/packages/<category>/<plugin>/<pkg>/package.json`.
*/
readonly root: string;
/** Default `"throw"`. */
readonly onError?: DiscoveryErrorMode;
/** Extension hints merged with `SERVER_DEP_HINTS` for tier inference. */
readonly tierHints?: ReadonlySet<string>;
}
export interface AsyncDiscoverOptions extends DiscoverOptions {
/** Aborts the walk; the iterator throws `OVERVIEW.DISCOVERY_ABORTED`. */
readonly signal?: AbortSignal;
}
/**
* Synchronously walk `<root>/packages/<category>/<plugin>/<pkg>/package.json`
* and return a fully-validated `SuavePlanManifest`.
*
* @throws OverviewError - `OVERVIEW.PARSE_FAILED` / `OVERVIEW.INVALID_RECORD`
* when `onError === "throw"` and a row fails parsing.
*/
export function discoverPackages(options: DiscoverOptions): SuavePlanManifest {
const start = nowMs();
const onError = options.onError ?? "throw";
const records: PackageRecord[] = [];
const packagesRoot = join(options.root, "packages");
if (existsSync(packagesRoot)) {
for (const category of readdirSync(packagesRoot)) {
const categoryPath = join(packagesRoot, category);
if (!statSync(categoryPath).isDirectory()) continue;
for (const plugin of readdirSync(categoryPath)) {
const pluginPath = join(categoryPath, plugin);
if (!statSync(pluginPath).isDirectory()) continue;
for (const pkgDir of readdirSync(pluginPath)) {
const pkgPath = join(pluginPath, pkgDir);
if (!statSync(pkgPath).isDirectory()) continue;
const rec = readRecord(
pkgPath,
category,
plugin,
pkgDir,
options.tierHints,
onError
);
if (rec !== undefined) records.push(rec);
}
}
}
}
records.sort((a, b) => a.name.localeCompare(b.name));
packagesDiscoveredTotal.add(records.length);
discoverDurationMs.record(nowMs() - start);
return assembleManifest(records);
}
/**
* AsyncGenerator variant that yields one `PackageRecord` at a time and
* honours `AbortSignal`. Aborting causes the next `.next()` call to throw
* `OVERVIEW.DISCOVERY_ABORTED`.
*/
export async function* discoverPackagesIter(
options: AsyncDiscoverOptions
): AsyncGenerator<PackageRecord> {
const onError = options.onError ?? "throw";
const packagesRoot = join(options.root, "packages");
if (!existsSync(packagesRoot)) return;
for (const category of readdirSync(packagesRoot)) {
const categoryPath = join(packagesRoot, category);
if (!statSync(categoryPath).isDirectory()) continue;
for (const plugin of readdirSync(categoryPath)) {
const pluginPath = join(categoryPath, plugin);
if (!statSync(pluginPath).isDirectory()) continue;
for (const pkgDir of readdirSync(pluginPath)) {
if (options.signal?.aborted === true) {
throw new OverviewError(
OVERVIEW_CODES.DISCOVERY_ABORTED,
"overview.discovery_aborted"
);
}
const pkgPath = join(pluginPath, pkgDir);
if (!statSync(pkgPath).isDirectory()) continue;
const rec = readRecord(
pkgPath,
category,
plugin,
pkgDir,
options.tierHints,
onError
);
if (rec !== undefined) yield rec;
}
}
}
}
function readRecord(
pkgPath: string,
category: string,
plugin: string,
dirName: string,
tierHints: ReadonlySet<string> | undefined,
onError: DiscoveryErrorMode
): PackageRecord | undefined {
const file = join(pkgPath, "package.json");
if (!existsSync(file)) return undefined;
let raw: RawPackageJson;
try {
raw = JSON.parse(readFileSync(file, "utf8")) as RawPackageJson;
} catch (cause) {
recordsRejectedTotal.add(1, { reason: "parse_failed" });
if (onError === "throw") {
throw new OverviewError(
OVERVIEW_CODES.PARSE_FAILED,
"overview.parse_failed",
{
cause: cause as Error,
context: { file },
}
);
}
return undefined;
}
const name = raw.name ?? "";
const short = name.startsWith("@suaveplan/")
? name.slice("@suaveplan/".length)
: name;
const tier: Tier = inferTier(short, raw, tierHints);
const workspaceDeps = collectWorkspaceDeps(raw);
const candidate: PackageRecord = {
name,
short,
category,
plugin,
dirName,
version: raw.version ?? "",
description: raw.description ?? "",
tier,
workspaceDeps,
hasBin: raw.bin !== undefined,
hasBrowserField: raw.browser !== undefined,
};
const parsed = PackageRecordSchema.safeParse(candidate);
if (!parsed.success) {
recordsRejectedTotal.add(1, { reason: "invalid_record" });
if (onError === "throw") {
throw new OverviewError(
OVERVIEW_CODES.INVALID_RECORD,
"overview.invalid_record",
{ context: { file, issues: parsed.error.issues } }
);
}
return undefined;
}
return parsed.data;
}
function collectWorkspaceDeps(p: RawPackageJson): string[] {
const out = new Set<string>();
for (const map of [
p.dependencies,
p.peerDependencies,
p.optionalDependencies,
]) {
if (!map) continue;
for (const [depName, range] of Object.entries(map)) {
if (
typeof range === "string" &&
range.startsWith("workspace:") &&
depName.startsWith("@suaveplan/")
) {
out.add(depName);
}
}
}
return [...out].sort();
}
function assembleManifest(records: PackageRecord[]): SuavePlanManifest {
const categories = [...new Set(records.map((r) => r.category))].sort();
const tierCounts: Record<Tier, number> = {
python: 0,
r3f: 0,
react: 0,
browser: 0,
server: 0,
universal: 0,
};
for (const r of records) tierCounts[r.tier] += 1;
const plugins = byPlugin(records);
return { packages: records, plugins, categories, tierCounts };
}
function nowMs(): number {
return Date.now();
}

View File

@@ -0,0 +1,86 @@
# `errors/errors.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the stable error-code registry and the concrete `OverviewError`
subclass of `SuaveplanError`. Every public entry point in `@suaveplan/overview`
that throws does so through this class, so callers can rely on a single
`instanceof OverviewError` check and a literal-typed `code` field for
discrimination.
The registry has six entries — one per failure mode that the package can
detect — and the §2c "never delete spec elements to satisfy a gate" rule means
each entry is exercised by a real call site somewhere in `src/`, asserted by
the static test `every code has a real call site in src/`.
## Features
- Six stable codes registered through `defineCodes("OVERVIEW", [...])`
- Frozen, fully-inferred code record — `OVERVIEW_CODES.PARSE_FAILED` is the
literal `"OVERVIEW.PARSE_FAILED"`
- `OverviewCode` closed union for exhaustive switch statements
- `OverviewError` extends `SuaveplanError<OverviewCode>` so `instanceof`
narrows `error.code`
- Preserves cause chain, context, and trace-id propagation via
`SuaveplanErrorOptions`
## Usage: Basic
```ts
import { OverviewError, OVERVIEW_CODES } from "@suaveplan/overview";
throw new OverviewError(
OVERVIEW_CODES.UNKNOWN_PACKAGE,
"overview.unknown_package",
{ context: { pkg: "@suaveplan/missing" } },
);
```
## Usage: Advanced
```ts
import { OverviewError } from "@suaveplan/overview";
try {
// ...
} catch (err) {
if (err instanceof OverviewError) {
switch (err.code) {
case "OVERVIEW.CYCLE_DETECTED":
// type-narrowed; safe to read err.context?.cycle
break;
case "OVERVIEW.PARSE_FAILED":
break;
default:
break;
}
}
throw err;
}
```
The exhaustive `switch` benefits from `OverviewCode` being a closed union —
adding a new code without handling it in a downstream `switch` is a TypeScript
error.
## API Reference
- `OVERVIEW_CODES` — `Readonly<{PARSE_FAILED, INVALID_RECORD, CYCLE_DETECTED,
UNKNOWN_PACKAGE, DISCOVERY_ABORTED, INVALID_TIER}>` with literal `OVERVIEW.<X>`
values
- `type OverviewCode` — closed union of every value in `OVERVIEW_CODES`
- `class OverviewError extends SuaveplanError<OverviewCode>` — constructor
`(code, messageKey, options?)`
## Implementation Notes
The `defineCodes` helper from `@suaveplan/error` freezes its return value so the
registry cannot be mutated at runtime. `OverviewError` deliberately re-exposes
the `protected` constructor of `SuaveplanError` as `public` (the language
requires each subclass to do this explicitly) so callers can `new` an instance
directly without a factory. The class is final by convention — sub-classing it
would dilute `instanceof` narrowing.

View File

@@ -0,0 +1,61 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { OVERVIEW_CODES, OverviewError } from "./errors.js";
describe("OVERVIEW_CODES registry", () => {
it("every OVERVIEW code is unique", { timeout: 5000 }, () => {
const values = Object.values(OVERVIEW_CODES);
const unique = new Set(values);
expect(unique.size).toBe(values.length);
expect(values.length).toBe(6);
for (const v of values) {
expect(v.startsWith("OVERVIEW.")).toBe(true);
}
});
it("instances carry the literal code and a cause chain", {
timeout: 5000,
}, () => {
const cause = new Error("root");
const err = new OverviewError(
OVERVIEW_CODES.PARSE_FAILED,
"overview.parse_failed",
{ cause, context: { path: "/tmp/x" } }
);
expect(err).toBeInstanceOf(OverviewError);
expect(err.code).toBe("OVERVIEW.PARSE_FAILED");
expect(err.cause).toBe(cause);
expect(err.context?.path).toBe("/tmp/x");
});
it("every code has a real call site in src/", { timeout: 5000 }, () => {
const root = join(__dirname, "..");
const sources = collectTsFiles(root);
const concat = sources
.filter((f) => !f.endsWith(".test.ts"))
.map((f) => readFileSync(f, "utf8"))
.join("\n");
for (const code of Object.values(OVERVIEW_CODES)) {
const constant = `OVERVIEW_CODES.${code.slice("OVERVIEW.".length)}`;
const directLiteral = `"${code}"`;
const hit =
concat.includes(constant) || concat.includes(directLiteral);
expect(hit, `code ${code} must appear in src/`).toBe(true);
}
});
});
function collectTsFiles(dir: string): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
const st = statSync(full);
if (st.isDirectory()) {
out.push(...collectTsFiles(full));
continue;
}
if (full.endsWith(".ts")) out.push(full);
}
return out;
}

View File

@@ -0,0 +1,45 @@
import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error code registry for `@suaveplan/overview`.
*
* Each code is reachable from a real call site in `src/`:
*
* - `OVERVIEW.PARSE_FAILED` — `discover` saw a `package.json` whose contents
* failed `JSON.parse`.
* - `OVERVIEW.INVALID_RECORD` — a parsed object failed Zod validation against
* `PackageRecordSchema`.
* - `OVERVIEW.CYCLE_DETECTED` — `topologicalOrder` ran into a cycle.
* - `OVERVIEW.UNKNOWN_PACKAGE` — a graph query referenced a package not
* present in the graph.
* - `OVERVIEW.DISCOVERY_ABORTED` — `discoverPackagesIter` observed an aborted
* `AbortSignal`.
* - `OVERVIEW.INVALID_TIER` — a non-`Tier` string was passed somewhere that
* needed a tier label (e.g. `byTier` initialiser given an extension).
*/
export const OVERVIEW_CODES = defineCodes("OVERVIEW", [
"PARSE_FAILED",
"INVALID_RECORD",
"CYCLE_DETECTED",
"UNKNOWN_PACKAGE",
"DISCOVERY_ABORTED",
"INVALID_TIER",
] as const);
/** Closed union of every code emitted by the package. */
export type OverviewCode = (typeof OVERVIEW_CODES)[keyof typeof OVERVIEW_CODES];
/**
* Concrete error class. Carries the literal `code` union so `instanceof`
* checks narrow to a typed code.
*/
export class OverviewError extends SuaveplanError<OverviewCode> {
constructor(
code: OverviewCode,
messageKey: string,
options: SuaveplanErrorOptions = {}
) {
super(code, messageKey, options);
}
}

View File

@@ -0,0 +1,90 @@
# `graph/graph.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the graph utilities that operate on a `WorkspaceGraph` built from a
list of `PackageRecord`s. Covers the analyses every project tool eventually
needs: build a forward / reverse adjacency map, compute a topological
dependency-first order, detect cycles, gather transitive deps or dependents,
and identify graph roots (no deps) and leaves (no dependents).
Edges where the target is not also a node are silently dropped at build time.
This is the same posture as `scripts/folder-reorg/generate-manifest.ts` —
necessary because Suaveplan migrates in waves and partial-manifest
snapshots are routine.
## Features
- `buildGraph` — populates forward and reverse adjacency in one pass; emits
`overview.graph_builds_total`
- `topologicalOrder` — Kahn's algorithm with stable lexicographic tie-breaking;
throws `OVERVIEW.CYCLE_DETECTED` on a cycle (error carries `context.cycle`)
- `detectCycles` — Tarjan's SCC, returns every cycle as a sorted `string[]`
- `transitiveDependencies` / `transitiveDependents` — BFS over the appropriate
direction; throws `OVERVIEW.UNKNOWN_PACKAGE` for missing inputs
- `leafPackages` — no dependents (no incoming edges)
- `rootPackages` — no deps (no outgoing edges)
## Usage: Basic
```ts
import { buildGraph, topologicalOrder } from "@suaveplan/overview";
const graph = buildGraph(records);
const order = topologicalOrder(graph);
// `order[0]` has no workspace deps; the last entry depends on everything.
```
## Usage: Advanced
```ts
import {
buildGraph,
detectCycles,
transitiveDependencies,
transitiveDependents,
leafPackages,
rootPackages,
} from "@suaveplan/overview";
const graph = buildGraph(records);
const cycles = detectCycles(graph);
if (cycles.length > 0) {
console.error("Cycles detected:", cycles);
}
const blastRadius = transitiveDependents(graph, "@suaveplan/error");
const supplyChain = transitiveDependencies(graph, "@suaveplan/api");
const leaves = leafPackages(graph);
const roots = rootPackages(graph);
```
`topologicalOrder` is deterministic — two callers with the same graph see the
same order — so it can drive reproducible build plans and CI fan-outs.
## API Reference
- `buildGraph(records): WorkspaceGraph`
- `topologicalOrder(graph): string[]` — throws `OVERVIEW.CYCLE_DETECTED`
- `detectCycles(graph): string[][]`
- `transitiveDependencies(graph, pkg): string[]` — throws
`OVERVIEW.UNKNOWN_PACKAGE`
- `transitiveDependents(graph, pkg): string[]` — throws
`OVERVIEW.UNKNOWN_PACKAGE`
- `leafPackages(graph): string[]`
- `rootPackages(graph): string[]`
## Implementation Notes
`topologicalOrder` initialises a remaining-out-degree counter per node and
drains nodes with zero out-degree first; the queue is kept sorted on insertion
so the output is stable across runs. `detectCycles` uses a recursive
Tarjan implementation — the workspace size is bounded (hundreds of packages),
so the stack-depth budget is comfortable. BFS in `transitiveDependencies` /
`transitiveDependents` uses an array-based queue with `Array.prototype.shift`
— acceptable at workspace scale; if the package ever has to handle tens of
thousands of nodes the queue can be swapped for a head/tail index pair.

View File

@@ -0,0 +1,297 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import fc from "fast-check";
import { OverviewError } from "../errors/errors.js";
import type { PackageRecord } from "../types/types.js";
import {
buildGraph,
detectCycles,
leafPackages,
rootPackages,
topologicalOrder,
transitiveDependencies,
transitiveDependents,
} from "./graph.js";
function rec(name: string, deps: readonly string[] = []): PackageRecord {
return {
name: `@suaveplan/${name}`,
short: name,
category: "foundation",
plugin: "core",
dirName: name,
version: "0.1.0",
description: "",
tier: "universal",
workspaceDeps: deps.map((d) => `@suaveplan/${d}`),
hasBin: false,
hasBrowserField: false,
};
}
describe("buildGraph", () => {
it("drops edges with unknown targets", { timeout: 5000 }, () => {
const g = buildGraph([rec("a", ["b", "missing"]), rec("b")]);
expect([...(g.forward.get("@suaveplan/a") ?? [])]).toEqual([
"@suaveplan/b",
]);
expect(g.nodes.has("@suaveplan/missing")).toBe(false);
});
it("builds reverse adjacency", { timeout: 5000 }, () => {
const g = buildGraph([rec("a", ["b"]), rec("b")]);
expect([...(g.reverse.get("@suaveplan/b") ?? [])]).toEqual([
"@suaveplan/a",
]);
});
});
describe("topologicalOrder", () => {
it("respects dependency direction", { timeout: 5000 }, () => {
// a -> b -> c
const g = buildGraph([rec("a", ["b"]), rec("b", ["c"]), rec("c")]);
const order = topologicalOrder(g);
expect(order).toEqual(["@suaveplan/c", "@suaveplan/b", "@suaveplan/a"]);
});
it("is stable on ties (lexicographic)", { timeout: 5000 }, () => {
const g = buildGraph([rec("a"), rec("b")]);
const order = topologicalOrder(g);
expect(order).toEqual(["@suaveplan/a", "@suaveplan/b"]);
});
it("throws CYCLE_DETECTED on a cycle", { timeout: 5000 }, () => {
// a -> b -> a
const g = buildGraph([rec("a", ["b"]), rec("b", ["a"])]);
try {
topologicalOrder(g);
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.CYCLE_DETECTED");
expect(err.context?.cycle).toContain("@suaveplan/a");
expect(err.context?.cycle).toContain("@suaveplan/b");
}
}
});
it("is deterministic on random DAGs (property)", { timeout: 10000 }, () => {
fc.assert(
fc.property(
fc
.uniqueArray(
fc
.string({ minLength: 1, maxLength: 5 })
.filter((s) => /^[a-z]+$/.test(s)),
{ minLength: 2, maxLength: 8 }
)
.chain((names) => {
// Build edges only from lower-index to higher-index
// names → guaranteed DAG.
return fc.tuple(
fc.constant(names),
fc.array(
fc.tuple(
fc.integer({
min: 0,
max: names.length - 1,
}),
fc.integer({
min: 0,
max: names.length - 1,
})
),
{ maxLength: 10 }
)
);
}),
([names, edges]) => {
const records = names.map((n) => {
const deps = edges
.filter(
([from, to]) => from < to && names[from] === n
)
.map(([, to]) => names[to]);
return rec(n, deps);
});
const graph = buildGraph(records);
const order = topologicalOrder(graph);
const position = new Map(order.map((n, i) => [n, i]));
for (const r of records) {
for (const dep of r.workspaceDeps) {
if (!graph.nodes.has(dep)) continue;
expect(
(position.get(dep) as number) <
(position.get(r.name) as number)
).toBe(true);
}
}
}
),
{ numRuns: 25 }
);
});
});
describe("detectCycles", () => {
it("returns [] on an acyclic graph", { timeout: 5000 }, () => {
const g = buildGraph([rec("a", ["b"]), rec("b")]);
expect(detectCycles(g)).toEqual([]);
});
it("returns the SCC for a 2-cycle", { timeout: 5000 }, () => {
const g = buildGraph([rec("a", ["b"]), rec("b", ["a"])]);
const cycles = detectCycles(g);
expect(cycles.length).toBe(1);
expect(cycles[0]).toEqual(["@suaveplan/a", "@suaveplan/b"]);
});
it("returns multiple SCCs sorted by leader", { timeout: 5000 }, () => {
// a<->b plus c<->d
const g = buildGraph([
rec("a", ["b"]),
rec("b", ["a"]),
rec("c", ["d"]),
rec("d", ["c"]),
]);
const cycles = detectCycles(g);
expect(cycles.length).toBe(2);
expect(cycles[0][0]).toBe("@suaveplan/a");
expect(cycles[1][0]).toBe("@suaveplan/c");
});
});
describe("transitive closures", () => {
it("transitiveDependencies BFS closure", { timeout: 5000 }, () => {
// a -> b, b -> c, b -> d
const g = buildGraph([
rec("a", ["b"]),
rec("b", ["c", "d"]),
rec("c"),
rec("d"),
]);
expect(transitiveDependencies(g, "@suaveplan/a")).toEqual([
"@suaveplan/b",
"@suaveplan/c",
"@suaveplan/d",
]);
});
it("transitiveDependents BFS closure", { timeout: 5000 }, () => {
const g = buildGraph([rec("a", ["b"]), rec("b", ["c"]), rec("c")]);
expect(transitiveDependents(g, "@suaveplan/c")).toEqual([
"@suaveplan/a",
"@suaveplan/b",
]);
});
it("transitiveDependencies throws UNKNOWN_PACKAGE", {
timeout: 5000,
}, () => {
const g = buildGraph([rec("a")]);
try {
transitiveDependencies(g, "@suaveplan/missing");
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.UNKNOWN_PACKAGE");
}
}
});
it("transitiveDependents throws UNKNOWN_PACKAGE", { timeout: 5000 }, () => {
const g = buildGraph([rec("a")]);
try {
transitiveDependents(g, "@suaveplan/missing");
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
}
});
});
describe("leaves and roots", () => {
it("leaves and roots on a star", { timeout: 5000 }, () => {
// hub depends on a, b, c
const g = buildGraph([
rec("hub", ["a", "b", "c"]),
rec("a"),
rec("b"),
rec("c"),
]);
// dependents direction roots: hub (no dependents)
expect(leafPackages(g)).toEqual(["@suaveplan/hub"]);
// dependency direction roots: a, b, c (no deps)
expect(rootPackages(g)).toEqual([
"@suaveplan/a",
"@suaveplan/b",
"@suaveplan/c",
]);
});
it("transitiveDependencies handles diamond graphs (revisit dedup)", {
timeout: 5000,
}, () => {
// a -> b, a -> c, b -> d, c -> d (diamond)
const g = buildGraph([
rec("a", ["b", "c"]),
rec("b", ["d"]),
rec("c", ["d"]),
rec("d"),
]);
expect(transitiveDependencies(g, "@suaveplan/a")).toEqual([
"@suaveplan/b",
"@suaveplan/c",
"@suaveplan/d",
]);
});
it("detectCycles handles cross-edges to a finished SCC", {
timeout: 5000,
}, () => {
// a -> b, a -> c, c -> b. When a iterates its edges (a->b
// then a->c after recursion, or in some order) we encounter a
// node that is already indexed and NOT on the stack — the
// else branch of the inner `else if (onStack.has(w))`.
const g = buildGraph([rec("a", ["b", "c"]), rec("b"), rec("c", ["b"])]);
expect(detectCycles(g)).toEqual([]);
});
it("detectCycles handles back-edges to an already-indexed node", {
timeout: 5000,
}, () => {
// a -> b, b -> c, c -> a (3-cycle exercising the on-stack
// back-edge branch of Tarjan)
const g = buildGraph([
rec("a", ["b"]),
rec("b", ["c"]),
rec("c", ["a"]),
]);
const cycles = detectCycles(g);
expect(cycles.length).toBe(1);
expect(cycles[0]).toEqual([
"@suaveplan/a",
"@suaveplan/b",
"@suaveplan/c",
]);
});
it("tolerates a sparse WorkspaceGraph (missing adjacency keys)", {
timeout: 5000,
}, () => {
// Hand-craft a graph where `forward`/`reverse` don't carry an
// entry for every node — exercises the getSet undefined branch.
const graph = {
nodes: new Set(["@suaveplan/a"]),
forward: new Map<string, Set<string>>(),
reverse: new Map<string, Set<string>>(),
};
expect(rootPackages(graph)).toEqual(["@suaveplan/a"]);
expect(leafPackages(graph)).toEqual(["@suaveplan/a"]);
expect(topologicalOrder(graph)).toEqual(["@suaveplan/a"]);
expect(detectCycles(graph)).toEqual([]);
expect(transitiveDependencies(graph, "@suaveplan/a")).toEqual([]);
expect(transitiveDependents(graph, "@suaveplan/a")).toEqual([]);
});
});

View File

@@ -0,0 +1,225 @@
import { OVERVIEW_CODES, OverviewError } from "../errors/errors.js";
import { graphBuildsTotal } from "../telemetry/telemetry.js";
import type { PackageRecord, WorkspaceGraph } from "../types/types.js";
/**
* Build a `WorkspaceGraph` from a list of records. Edges where the target
* is not in the node set are silently dropped — this matches the discovery
* behaviour of `scripts/folder-reorg/generate-manifest.ts` so partial-
* migration manifests stay self-consistent.
*
* Emits `overview.graph_builds_total`.
*/
export function buildGraph(records: readonly PackageRecord[]): WorkspaceGraph {
const nodes = new Set<string>();
const forward = new Map<string, Set<string>>();
const reverse = new Map<string, Set<string>>();
for (const r of records) {
nodes.add(r.name);
ensureSet(forward, r.name);
ensureSet(reverse, r.name);
}
for (const r of records) {
for (const dep of r.workspaceDeps) {
if (!nodes.has(dep)) continue;
ensureSet(forward, r.name).add(dep);
ensureSet(reverse, dep).add(r.name);
}
}
graphBuildsTotal.add(1);
return { nodes, forward, reverse };
}
/**
* Topological order using Kahn's algorithm. Returns packages in
* **dep-first** order: a node appears after its workspace dependencies
* (so the very last node has no dependencies of its own).
*
* Throws `OVERVIEW.CYCLE_DETECTED` when a cycle is present; the error's
* `context.cycle` lists the nodes that could not be ordered.
*/
export function topologicalOrder(graph: WorkspaceGraph): string[] {
const remaining = new Map<string, number>();
for (const node of graph.nodes) {
remaining.set(node, getSet(graph.forward, node).size);
}
const queue: string[] = [];
for (const [node, count] of remaining) {
if (count === 0) queue.push(node);
}
queue.sort();
const order: string[] = [];
while (queue.length > 0) {
const node = queue.shift() as string;
order.push(node);
const dependents = getSet(graph.reverse, node);
const batch: string[] = [];
for (const dependent of dependents) {
const next = (remaining.get(dependent) as number) - 1;
remaining.set(dependent, next);
if (next === 0) batch.push(dependent);
}
batch.sort();
for (const b of batch) queue.push(b);
}
if (order.length !== graph.nodes.size) {
const cycle = [...graph.nodes].filter(
(n) => (remaining.get(n) as number) > 0
);
throw new OverviewError(
OVERVIEW_CODES.CYCLE_DETECTED,
"overview.cycle_detected",
{ context: { cycle } }
);
}
return order;
}
/**
* Detect strongly-connected components larger than one node (Tarjan).
* Returns an array of cycle arrays — empty when the graph is acyclic.
*/
export function detectCycles(graph: WorkspaceGraph): string[][] {
const index = new Map<string, number>();
const lowlink = new Map<string, number>();
const onStack = new Set<string>();
const stack: string[] = [];
const result: string[][] = [];
let counter = 0;
const strongConnect = (v: string): void => {
index.set(v, counter);
lowlink.set(v, counter);
counter += 1;
stack.push(v);
onStack.add(v);
for (const w of getSet(graph.forward, v)) {
if (!index.has(w)) {
strongConnect(w);
lowlink.set(
v,
Math.min(lowlink.get(v) as number, lowlink.get(w) as number)
);
} else if (onStack.has(w)) {
lowlink.set(
v,
Math.min(lowlink.get(v) as number, index.get(w) as number)
);
}
}
if (lowlink.get(v) === index.get(v)) {
const component: string[] = [];
for (;;) {
const w = stack.pop() as string;
onStack.delete(w);
component.push(w);
if (w === v) break;
}
if (component.length > 1) result.push(component.sort());
}
};
for (const node of graph.nodes) {
if (!index.has(node)) strongConnect(node);
}
return result.sort((a, b) =>
(a[0] as string).localeCompare(b[0] as string)
);
}
/**
* Sorted transitive workspace dependencies of `pkg`. Throws
* `OVERVIEW.UNKNOWN_PACKAGE` when `pkg` is not in the graph.
*/
export function transitiveDependencies(
graph: WorkspaceGraph,
pkg: string
): string[] {
ensureKnown(graph, pkg);
return bfs(graph.forward, pkg);
}
/**
* Sorted transitive dependents of `pkg`. Throws
* `OVERVIEW.UNKNOWN_PACKAGE` when `pkg` is not in the graph.
*/
export function transitiveDependents(
graph: WorkspaceGraph,
pkg: string
): string[] {
ensureKnown(graph, pkg);
return bfs(graph.reverse, pkg);
}
/**
* Sorted list of packages with no dependents (nothing else imports them).
* Roots of the dependent-direction DAG.
*/
export function leafPackages(graph: WorkspaceGraph): string[] {
const out: string[] = [];
for (const node of graph.nodes) {
if (getSet(graph.reverse, node).size === 0) out.push(node);
}
return out.sort();
}
/**
* Sorted list of packages with no workspace deps. Roots of the
* dependency-direction DAG.
*/
export function rootPackages(graph: WorkspaceGraph): string[] {
const out: string[] = [];
for (const node of graph.nodes) {
if (getSet(graph.forward, node).size === 0) out.push(node);
}
return out.sort();
}
function ensureKnown(graph: WorkspaceGraph, pkg: string): void {
if (graph.nodes.has(pkg)) return;
throw new OverviewError(
OVERVIEW_CODES.UNKNOWN_PACKAGE,
"overview.unknown_package",
{ context: { pkg } }
);
}
function bfs(
adjacency: ReadonlyMap<string, ReadonlySet<string>>,
start: string
): string[] {
const seen = new Set<string>();
const queue: string[] = [start];
while (queue.length > 0) {
const node = queue.shift() as string;
for (const neighbour of getSet(adjacency, node)) {
if (seen.has(neighbour)) continue;
seen.add(neighbour);
queue.push(neighbour);
}
}
return [...seen].sort();
}
function ensureSet(map: Map<string, Set<string>>, key: string): Set<string> {
let s = map.get(key);
if (s === undefined) {
s = new Set<string>();
map.set(key, s);
}
return s;
}
function getSet(
map: ReadonlyMap<string, ReadonlySet<string>>,
key: string
): ReadonlySet<string> {
const s = map.get(key);
return s === undefined ? EMPTY_SET : s;
}
const EMPTY_SET: ReadonlySet<string> = new Set<string>();

View File

@@ -0,0 +1,52 @@
// Errors
export {
OVERVIEW_CODES,
type OverviewCode,
OverviewError,
} from "./errors/errors.js";
// Graph
export {
buildGraph,
detectCycles,
leafPackages,
rootPackages,
topologicalOrder,
transitiveDependencies,
transitiveDependents,
} from "./graph/graph.js";
// Rollups
export { byCategory, byPlugin, byTier } from "./rollups/rollups.js";
// Schemas
export {
SuavePlanManifestSchema,
PackageRecordSchema,
PluginSummarySchema,
parseTier,
TierSchema,
} from "./schemas/schemas.js";
// Telemetry instruments (re-exported so consumers can read them without
// reaching into the sub-path)
export {
discoverDurationMs,
graphBuildsTotal,
packagesDiscoveredTotal,
type RejectionReason,
recordsRejectedTotal,
} from "./telemetry/telemetry.js";
// Tier
export { inferTier, SERVER_DEP_HINTS } from "./tier/tier.js";
// Types
export type {
SuavePlanManifest,
PackageRecord,
PluginSummary,
RawPackageJson,
Tier,
WorkspaceGraph,
} from "./types/types.js";
export { TIERS } from "./types/types.js";
// Note: discoverPackages + discoverPackagesIter live behind the
// `@suaveplan/overview/discover` sub-path export so the root entry stays
// universal (no `node:fs` imports).

View File

@@ -0,0 +1,62 @@
# `rollups/rollups.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the three aggregate views over a `PackageRecord[]` that the manifest
needs: plugin roll-ups (one row per `(category, plugin)`), category counts
(simple histogram), and tier bucketing (every tier key populated, even when
empty). These are pure functions with no side effects, so callers can run them
repeatedly without consulting the disk.
The roll-ups mirror the data that the human-readable `MANIFEST.md` table
produces, but typed: downstream tooling can consume them directly instead of
re-parsing the Markdown.
## Features
- `byPlugin` — sorted `PluginSummary[]`
- `byCategory` — `Record<string, number>` of per-category package counts
- `byTier` — `Record<Tier, PackageRecord[]>` with every tier key present
## Usage: Basic
```ts
import { byPlugin, byCategory, byTier } from "@suaveplan/overview";
const plugins = byPlugin(records);
const counts = byCategory(records);
const tiered = byTier(records);
```
## Usage: Advanced
```ts
import { byTier, TIERS } from "@suaveplan/overview";
const buckets = byTier(records);
for (const tier of TIERS) {
console.log(`${tier}: ${buckets[tier].length}`);
}
```
The `byTier` output always has every tier key, so downstream code never has to
guard against missing keys — a tier with zero packages simply maps to `[]`.
## API Reference
- `byPlugin(records): PluginSummary[]` — sorted by `"<category>/<plugin>"`
- `byCategory(records): Record<string, number>`
- `byTier(records): Record<Tier, PackageRecord[]>` — covers all six tier keys
## Implementation Notes
`byPlugin` accumulates into a `Map` keyed by `"<category>/<plugin>"`, then
sorts by key on emit. `byTier` initialises the output via
`Object.fromEntries(TIERS.map(...))` so the result has a stable insertion
order matching `TIERS`. None of the helpers emit telemetry on their own — they
are pure transformations; instrumentation is the caller's responsibility if
needed.

View File

@@ -0,0 +1,88 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import type { PackageRecord, Tier } from "../types/types.js";
import { byCategory, byPlugin, byTier } from "./rollups.js";
function rec(
name: string,
category: string,
plugin: string,
tier: Tier,
deps: readonly string[] = []
): PackageRecord {
return {
name: `@suaveplan/${name}`,
short: name,
category,
plugin,
dirName: name,
version: "0.1.0",
description: "",
tier,
workspaceDeps: deps,
hasBin: false,
hasBrowserField: false,
};
}
describe("rollups", () => {
it("byPlugin aggregates packages + workspace deps", {
timeout: 5000,
}, () => {
const records = [
rec("a", "foundation", "core", "universal", [
"@suaveplan/x",
"@suaveplan/y",
]),
rec("b", "foundation", "core", "universal", [
"@suaveplan/x",
"@suaveplan/y",
"@suaveplan/z",
]),
rec("c", "media", "audio", "universal", ["@suaveplan/x"]),
];
const out = byPlugin(records);
expect(out).toEqual([
{
category: "foundation",
plugin: "core",
packages: 2,
workspaceDeps: 5,
},
{
category: "media",
plugin: "audio",
packages: 1,
workspaceDeps: 1,
},
]);
});
it("byCategory counts packages", { timeout: 5000 }, () => {
const records = [
rec("a", "foundation", "core", "universal"),
rec("b", "foundation", "core", "universal"),
rec("c", "media", "audio", "universal"),
];
expect(byCategory(records)).toEqual({ foundation: 2, media: 1 });
});
it("byTier covers every Tier key", { timeout: 5000 }, () => {
const out = byTier([]);
expect(out.universal).toEqual([]);
expect(out.browser).toEqual([]);
expect(out.react).toEqual([]);
expect(out.r3f).toEqual([]);
expect(out.server).toEqual([]);
expect(out.python).toEqual([]);
});
it("byTier buckets records", { timeout: 5000 }, () => {
const records = [
rec("a", "foundation", "core", "universal"),
rec("b", "ui", "react", "react"),
];
const out = byTier(records);
expect(out.universal.length).toBe(1);
expect(out.react.length).toBe(1);
});
});

View File

@@ -0,0 +1,67 @@
import {
type PackageRecord,
type PluginSummary,
TIERS,
type Tier,
} from "../types/types.js";
/**
* Group records by `(category, plugin)` and emit one `PluginSummary` per pair,
* sorted by `"<category>/<plugin>"`. The summary carries the package count and
* the sum of outgoing workspace edges.
*/
export function byPlugin(records: readonly PackageRecord[]): PluginSummary[] {
const acc = new Map<
string,
{
category: string;
plugin: string;
packages: number;
workspaceDeps: number;
}
>();
for (const r of records) {
const key = `${r.category}/${r.plugin}`;
const cur = acc.get(key) ?? {
category: r.category,
plugin: r.plugin,
packages: 0,
workspaceDeps: 0,
};
cur.packages += 1;
cur.workspaceDeps += r.workspaceDeps.length;
acc.set(key, cur);
}
return [...acc.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([, v]) => v);
}
/**
* Return `category → package count` rolled up across every record.
*/
export function byCategory(
records: readonly PackageRecord[]
): Record<string, number> {
const out: Record<string, number> = {};
for (const r of records) {
out[r.category] = (out[r.category] ?? 0) + 1;
}
return out;
}
/**
* Bucket records by tier. Every tier key is present in the returned record,
* even when the input has no rows for that tier — values default to `[]`.
*/
export function byTier(
records: readonly PackageRecord[]
): Record<Tier, PackageRecord[]> {
const out = Object.fromEntries(
TIERS.map((t) => [t, [] as PackageRecord[]])
) as Record<Tier, PackageRecord[]>;
for (const r of records) {
out[r.tier].push(r);
}
return out;
}

View File

@@ -0,0 +1,68 @@
# `schemas/schemas.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Zod schemas (and one helper, `parseTier`) for every public shape that
`@suaveplan/overview` produces or accepts. The schemas are the authoritative
runtime gate: any `PackageRecord` that does not parse against
`PackageRecordSchema` is rejected by `discoverPackages` with
`OVERVIEW.INVALID_RECORD`, and `parseTier` converts an unknown string into a
typed `Tier` or throws `OVERVIEW.INVALID_TIER`.
The schemas are kept strict (`.strict()`) so extra fields are rejected — a
downstream consumer that wants to attach extension data must clone the record
into its own shape rather than smuggle properties through the manifest.
## Features
- `TierSchema` — the closed `Tier` enum
- `PackageRecordSchema` — every required field, validated with `.strict()`
- `PluginSummarySchema` — for plugin roll-ups
- `SuavePlanManifestSchema` — the full snapshot, including all six tier-count
keys as required numbers
- `parseTier` — convenience wrapper that throws an `OverviewError` instead of
returning a Zod `safeParse` failure
## Usage: Basic
```ts
import { PackageRecordSchema } from "@suaveplan/overview";
const r = PackageRecordSchema.parse(rawRow);
```
## Usage: Advanced
```ts
import { SuavePlanManifestSchema, parseTier } from "@suaveplan/overview";
const manifest = SuavePlanManifestSchema.parse(jsonFromDisk);
const userInput: unknown = process.env.OVERVIEW_TIER;
const tier = parseTier(userInput); // throws OVERVIEW.INVALID_TIER on bad input
```
The strict schemas allow consumers to safely round-trip a manifest through JSON
without losing fields or accidentally importing arbitrary extensions: a malformed
JSON payload simply fails the parse.
## API Reference
- `TierSchema: z.ZodType<Tier>`
- `PackageRecordSchema: z.ZodObject<...>` (strict, eleven fields)
- `PluginSummarySchema: z.ZodObject<...>` (strict, four fields)
- `SuavePlanManifestSchema: z.ZodObject<...>` (strict, four fields)
- `parseTier(input: unknown): Tier` — throws `OverviewError`
(`OVERVIEW.INVALID_TIER`) on failure
## Implementation Notes
The schemas use `z.enum(TIERS as readonly [Tier, ...Tier[]])` so the source of
truth is the `TIERS` tuple in `types/types.ts`; adding a new tier requires
updating that tuple (and the `tierCounts` shape) — both compile-time changes
that the test suite catches. `parseTier` deliberately surfaces a package-shaped
error rather than re-throwing the Zod `ZodError` so call sites only ever deal
with one error class.

View File

@@ -0,0 +1,147 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { OverviewError } from "../errors/errors.js";
import {
SuavePlanManifestSchema,
PackageRecordSchema,
PluginSummarySchema,
parseTier,
TierSchema,
} from "./schemas.js";
const validRecord = {
name: "@suaveplan/x",
short: "x",
category: "foundation",
plugin: "core",
dirName: "x",
version: "0.1.0",
description: "",
tier: "universal" as const,
workspaceDeps: [] as readonly string[],
hasBin: false,
hasBrowserField: false,
};
describe("schemas", () => {
it("TierSchema accepts every tier", { timeout: 5000 }, () => {
for (const t of [
"python",
"r3f",
"react",
"browser",
"server",
"universal",
]) {
expect(TierSchema.safeParse(t).success).toBe(true);
}
expect(TierSchema.safeParse("rust").success).toBe(false);
});
it("PackageRecordSchema rejects bad input per field", {
timeout: 5000,
}, () => {
expect(PackageRecordSchema.safeParse(validRecord).success).toBe(true);
const baseline = validRecord;
const mutations: ReadonlyArray<Partial<typeof validRecord>> = [
{ name: "no-prefix" },
{ name: "" },
{ short: "" },
{ category: "" },
{ plugin: "" },
{ dirName: "" },
{ version: "" },
{ tier: "rust" as never },
{
workspaceDeps: ["foo"] as unknown as readonly string[],
},
{ hasBin: "yes" as never },
{ hasBrowserField: "yes" as never },
];
for (const m of mutations) {
const res = PackageRecordSchema.safeParse({ ...baseline, ...m });
expect(res.success).toBe(false);
}
});
it("PackageRecordSchema rejects unknown fields (strict)", {
timeout: 5000,
}, () => {
const r = PackageRecordSchema.safeParse({
...validRecord,
extra: "field",
});
expect(r.success).toBe(false);
});
it("PluginSummarySchema accepts valid rows", { timeout: 5000 }, () => {
const ok = PluginSummarySchema.safeParse({
category: "foundation",
plugin: "core",
packages: 3,
workspaceDeps: 5,
});
expect(ok.success).toBe(true);
expect(
PluginSummarySchema.safeParse({
category: "foundation",
plugin: "core",
packages: -1,
workspaceDeps: 0,
}).success
).toBe(false);
});
it("SuavePlanManifestSchema accepts a populated snapshot", {
timeout: 5000,
}, () => {
const ok = SuavePlanManifestSchema.safeParse({
packages: [validRecord],
plugins: [
{
category: "foundation",
plugin: "core",
packages: 1,
workspaceDeps: 0,
},
],
categories: ["foundation"],
tierCounts: {
python: 0,
r3f: 0,
react: 0,
browser: 0,
server: 0,
universal: 1,
},
});
expect(ok.success).toBe(true);
});
it("SuavePlanManifestSchema rejects missing tier key", {
timeout: 5000,
}, () => {
const r = SuavePlanManifestSchema.safeParse({
packages: [],
plugins: [],
categories: [],
tierCounts: { python: 0, r3f: 0, react: 0, browser: 0, server: 0 },
});
expect(r.success).toBe(false);
});
it("parseTier returns a typed tier", { timeout: 5000 }, () => {
expect(parseTier("universal")).toBe("universal");
});
it("parseTier throws INVALID_TIER for nonsense", { timeout: 5000 }, () => {
try {
parseTier("rust");
expect.fail("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(OverviewError);
if (err instanceof OverviewError) {
expect(err.code).toBe("OVERVIEW.INVALID_TIER");
}
}
});
});

View File

@@ -0,0 +1,79 @@
import z from "zod";
import { OVERVIEW_CODES, OverviewError } from "../errors/errors.js";
import { TIERS, type Tier } from "../types/types.js";
/**
* Zod schema for the closed `Tier` union. Throws `OVERVIEW.INVALID_TIER`
* when a parse is attempted against a non-tier string via `parseTier`.
*/
export const TierSchema: z.ZodType<Tier> = z.enum(
TIERS as readonly [Tier, ...Tier[]]
);
/**
* Zod schema for a single manifest row. Every field is required; arrays must
* contain unique sorted entries.
*/
export const PackageRecordSchema = z
.object({
name: z.string().min(1).startsWith("@suaveplan/"),
short: z.string().min(1),
category: z.string().min(1),
plugin: z.string().min(1),
dirName: z.string().min(1),
version: z.string().min(1),
description: z.string(),
tier: TierSchema,
workspaceDeps: z.array(z.string().startsWith("@suaveplan/")),
hasBin: z.boolean(),
hasBrowserField: z.boolean(),
})
.strict();
/**
* Zod schema for a plugin roll-up row.
*/
export const PluginSummarySchema = z
.object({
category: z.string().min(1),
plugin: z.string().min(1),
packages: z.number().int().nonnegative(),
workspaceDeps: z.number().int().nonnegative(),
})
.strict();
/**
* Zod schema for the full manifest snapshot. `tierCounts` carries an entry
* for every tier — universal, browser, react, r3f, server, python.
*/
export const SuavePlanManifestSchema = z
.object({
packages: z.array(PackageRecordSchema),
plugins: z.array(PluginSummarySchema),
categories: z.array(z.string().min(1)),
tierCounts: z.object({
python: z.number().int().nonnegative(),
r3f: z.number().int().nonnegative(),
react: z.number().int().nonnegative(),
browser: z.number().int().nonnegative(),
server: z.number().int().nonnegative(),
universal: z.number().int().nonnegative(),
}),
})
.strict();
/**
* Narrow-typed parse for tier strings. Throws `OVERVIEW.INVALID_TIER` on
* failure so callers don't have to translate a Zod error into a package error.
*/
export function parseTier(input: unknown): Tier {
const parsed = TierSchema.safeParse(input);
if (!parsed.success) {
throw new OverviewError(
OVERVIEW_CODES.INVALID_TIER,
"overview.invalid_tier",
{ context: { input: String(input) } }
);
}
return parsed.data;
}

View File

@@ -0,0 +1,73 @@
# `telemetry/telemetry.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the OpenTelemetry meter for `@suaveplan/overview` and exposes the
three counters and one histogram that the spec requires. The meter is
constructed lazily at module import time via
`metrics.getMeter("@suaveplan/overview", "0.1.0")` so a no-op OTel SDK is the
default — there is no instrumentation cost on the cold path when no SDK is
installed.
Every metric is exported as a typed `Counter` or `Histogram` so other modules
can `.add` / `.record` against them directly without going through a wrapper.
## Features
- Single meter scoped to the package name + version
- `overview.packages_discovered_total` — counter
- `overview.records_rejected_total{reason}` — counter (reason is one of
`parse_failed` | `invalid_record`)
- `overview.graph_builds_total` — counter
- `overview.discover_duration_ms` — histogram with millisecond unit
- `RejectionReason` typed alias for the `reason` attribute
## Usage: Basic
```ts
import { packagesDiscoveredTotal } from "@suaveplan/overview/telemetry";
packagesDiscoveredTotal.add(7);
```
## Usage: Advanced
```ts
import {
discoverDurationMs,
recordsRejectedTotal,
type RejectionReason,
} from "@suaveplan/overview/telemetry";
const start = performance.now();
const result = await runDiscovery();
discoverDurationMs.record(performance.now() - start);
if (!result.ok) {
const reason: RejectionReason = result.reason;
recordsRejectedTotal.add(1, { reason });
}
```
Plug `@opentelemetry/sdk-metrics` in your runtime to harvest these metrics; no
adapter glue is needed inside `@suaveplan/overview`.
## API Reference
- `packagesDiscoveredTotal: Counter`
- `recordsRejectedTotal: Counter` (attribute: `reason: "parse_failed" |
"invalid_record"`)
- `graphBuildsTotal: Counter`
- `discoverDurationMs: Histogram` (unit: `ms`)
- `type RejectionReason = "parse_failed" | "invalid_record"`
## Implementation Notes
The meter is obtained once at module load and cached as a `const`. With the
no-op SDK installed (the default in test environments without a configured
OTel SDK), every `.add` and `.record` call is a fast no-op — there is no need
to feature-flag instrumentation. Tests use the SDK's `InMemoryMetricReader`
to assert that the counters and histogram move when discovery runs.

View File

@@ -0,0 +1,55 @@
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { discoverPackages } from "../discover/discover.js";
import { buildGraph } from "../graph/graph.js";
import {
discoverDurationMs,
graphBuildsTotal,
packagesDiscoveredTotal,
recordsRejectedTotal,
} from "./telemetry.js";
describe("telemetry instruments", () => {
it("exposes the three counters and one histogram", {
timeout: 5000,
}, () => {
expect(packagesDiscoveredTotal).toBeDefined();
expect(recordsRejectedTotal).toBeDefined();
expect(graphBuildsTotal).toBeDefined();
expect(discoverDurationMs).toBeDefined();
});
it("discoverPackages emits counter + histogram", { timeout: 5000 }, () => {
const tmp = mkdtempSync(join(tmpdir(), "overview-telemetry-"));
const dir = join(tmp, "packages", "foundation", "core", "a");
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, "package.json"),
JSON.stringify({
name: "@suaveplan/a",
version: "0.1.0",
description: "",
})
);
expect(() => discoverPackages({ root: tmp })).not.toThrow();
});
it("discoverPackages records rejected_total on parse failure (skip mode)", {
timeout: 5000,
}, () => {
const tmp = mkdtempSync(join(tmpdir(), "overview-telemetry-"));
const dir = join(tmp, "packages", "foundation", "core", "broken");
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "package.json"), "{ not valid");
expect(() =>
discoverPackages({ root: tmp, onError: "skip" })
).not.toThrow();
});
it("buildGraph emits graph_builds_total", { timeout: 5000 }, () => {
const g = buildGraph([]);
expect(g.nodes.size).toBe(0);
});
});

View File

@@ -0,0 +1,57 @@
import { type Counter, type Histogram, metrics } from "@opentelemetry/api";
const METER_NAME = "@suaveplan/overview";
const METER_VERSION = "0.1.0";
const meter = metrics.getMeter(METER_NAME, METER_VERSION);
/**
* Total `PackageRecord` rows produced by `discoverPackages`. Incremented by
* `records.length` once per successful discovery.
*/
export const packagesDiscoveredTotal: Counter = meter.createCounter(
"overview.packages_discovered_total",
{
description:
"Total package records produced by overview.discoverPackages.",
}
);
/**
* Total records rejected during discovery, labelled by reason
* (`parse_failed` | `invalid_record`).
*/
export const recordsRejectedTotal: Counter = meter.createCounter(
"overview.records_rejected_total",
{
description:
"Records rejected during discovery (parse_failed | invalid_record).",
}
);
/**
* Total `buildGraph` invocations. Incremented once per call.
*/
export const graphBuildsTotal: Counter = meter.createCounter(
"overview.graph_builds_total",
{
description: "Total invocations of overview.buildGraph.",
}
);
/**
* Histogram of wall-clock duration of `discoverPackages` in milliseconds.
*/
export const discoverDurationMs: Histogram = meter.createHistogram(
"overview.discover_duration_ms",
{
description:
"Wall-clock duration of overview.discoverPackages in milliseconds.",
unit: "ms",
}
);
/**
* Allowed rejection-reason values for the `records_rejected_total` counter.
*/
export type RejectionReason = "parse_failed" | "invalid_record";

View File

@@ -0,0 +1,73 @@
# `tier/tier.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Computes the deterministic `Tier` label for a package given its shortname and
the parsed `package.json` shape. The inference order mirrors
`scripts/folder-reorg/generate-manifest.ts` exactly, so consumers can rely on
the library and the script producing identical tier assignments for the same
input.
The function is pure and synchronous. It takes no side-effects, so it is safe
to call from any tier — including the universal root barrel — even though
some of the dep hints it inspects are Node-only.
## Features
- Six-rung priority cascade — python → r3f → react → browser → server →
universal
- Exported `SERVER_DEP_HINTS` set for downstream introspection
- Optional `extraHints` parameter merges user-supplied server-tier dep names
with the built-in set
- Pure / synchronous — zero I/O, zero allocations on the universal fallback
path beyond the merged-hint set
## Usage: Basic
```ts
import { inferTier } from "@suaveplan/overview";
const tier = inferTier("foo", {
dependencies: { "node:fs": "*" },
});
// tier === "server"
```
## Usage: Advanced
```ts
import { inferTier, SERVER_DEP_HINTS } from "@suaveplan/overview";
const extra = new Set(["custom-native-binding"]);
const tier = inferTier(
"thing",
{ dependencies: { "custom-native-binding": "*" } },
extra,
);
// SERVER_DEP_HINTS exposes the built-in set for tooling that wants to
// surface the rule set in a UI without re-declaring it.
console.log([...SERVER_DEP_HINTS]);
```
The optional third argument is the path for tooling that wants to extend the
hint set (e.g. a project-local native-binding registry) without forking the
library. The base set is read-only and never mutated.
## API Reference
- `inferTier(short: string, pkg: RawPackageJson, extraHints?: ReadonlySet<string>): Tier`
- `const SERVER_DEP_HINTS: ReadonlySet<string>` — eighteen built-in entries
spanning `node:*` modules and known native bindings
## Implementation Notes
The function performs at most one shallow `Object.keys` pass over each of
`peerDependencies`, `engines`, `dependencies`, and `devDependencies` — O(1)
work per `package.json` in practice. The `mergeHints` helper allocates a new
`Set` only when `extraHints` carries entries; otherwise it returns the base
set unchanged so the common path is allocation-free.

View File

@@ -0,0 +1,76 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { inferTier, SERVER_DEP_HINTS } from "./tier.js";
describe("inferTier", () => {
it("prioritises python > r3f > react > browser > server > universal", {
timeout: 5000,
}, () => {
// python by shortname prefix
expect(inferTier("python-foo", {})).toBe("python");
// python by field
expect(inferTier("foo", { python: {} })).toBe("python");
// r3f via peer dep
expect(inferTier("foo", { peerDependencies: { three: "*" } })).toBe(
"r3f"
);
// r3f via @react-three/fiber
expect(
inferTier("foo", {
peerDependencies: { "@react-three/fiber": "*" },
})
).toBe("r3f");
// r3f via shortname prefix
expect(inferTier("r3f-physics", {})).toBe("r3f");
// react via peer dep
expect(inferTier("foo", { peerDependencies: { react: "*" } })).toBe(
"react"
);
// react via shortname prefix
expect(inferTier("react-form", {})).toBe("react");
// browser via `browser` field
expect(inferTier("foo", { browser: true })).toBe("browser");
// browser via engines set without node/bun
expect(inferTier("foo", { engines: { workerd: ">=1" } })).toBe(
"browser"
);
// server via bin
expect(inferTier("foo", { bin: "./cli.js" })).toBe("server");
// server via dep hint
expect(inferTier("foo", { dependencies: { "node:fs": "*" } })).toBe(
"server"
);
expect(inferTier("foo", { devDependencies: { pg: "*" } })).toBe(
"server"
);
// universal fallback
expect(inferTier("foo", {})).toBe("universal");
// universal with node engine listed
expect(inferTier("foo", { engines: { node: ">=20" } })).toBe(
"universal"
);
// universal with bun engine listed
expect(inferTier("foo", { engines: { bun: ">=1" } })).toBe("universal");
});
it("honours extension hints", { timeout: 5000 }, () => {
const hints = new Set(["custom-native"]);
expect(
inferTier("foo", { dependencies: { "custom-native": "*" } }, hints)
).toBe("server");
// empty extension set passes through to fallback
expect(inferTier("foo", {}, new Set())).toBe("universal");
});
it("SERVER_DEP_HINTS contains the canonical Node namespaces", {
timeout: 5000,
}, () => {
expect(SERVER_DEP_HINTS.has("node:fs")).toBe(true);
expect(SERVER_DEP_HINTS.has("pg")).toBe(true);
expect(SERVER_DEP_HINTS.has("bun:sqlite")).toBe(true);
});
});

View File

@@ -0,0 +1,96 @@
import type { RawPackageJson, Tier } from "../types/types.js";
/**
* Server-only dependency names. When a package's `dependencies` /
* `devDependencies` includes any of these, `inferTier` returns `"server"`
* (unless an earlier rung already matched).
*
* Exported so downstream tooling can extend the set without forking — see
* `inferTier(..., extraHints)`.
*/
export const SERVER_DEP_HINTS: ReadonlySet<string> = new Set([
"bun:sqlite",
"node:fs",
"node:fs/promises",
"node:net",
"node:cluster",
"node:dgram",
"node:tls",
"node:dns",
"node:child_process",
"node:os",
"node:worker_threads",
"pg",
"ioredis",
"better-sqlite3",
"@sqlite.org/sqlite-wasm-node",
]);
/**
* Resolve a tier label from the package shortname and the parsed
* `package.json`. Priority order (top-down, first match wins):
*
* 1. `python` — `pkg.python` is set OR shortname starts with `python-`
* 2. `r3f` — peer deps include `@react-three/fiber` or `three` OR shortname
* starts with `r3f-`
* 3. `react` — peer deps include `react` OR shortname starts with `react-`
* 4. `browser` — `pkg.browser` is defined OR `engines` is set but neither
* `node` nor `bun` are listed
* 5. `server` — `pkg.bin` is defined OR any dep name appears in
* `SERVER_DEP_HINTS` (or `extraHints`)
* 6. `universal` — fallback
*
* @param short - npm shortname (after stripping `@suaveplan/`)
* @param pkg - the parsed `package.json` shape
* @param extraHints - optional extension set merged with `SERVER_DEP_HINTS`
*/
export function inferTier(
short: string,
pkg: RawPackageJson,
extraHints?: ReadonlySet<string>
): Tier {
if (pkg.python !== undefined || short.startsWith("python-"))
return "python";
const peerKeys = Object.keys(pkg.peerDependencies ?? {});
if (
peerKeys.includes("@react-three/fiber") ||
peerKeys.includes("three") ||
short.startsWith("r3f-")
) {
return "r3f";
}
if (peerKeys.includes("react") || short.startsWith("react-")) {
return "react";
}
if (pkg.browser !== undefined) return "browser";
const enginesKeys = Object.keys(pkg.engines ?? {});
if (
enginesKeys.length > 0 &&
!enginesKeys.includes("node") &&
!enginesKeys.includes("bun")
) {
return "browser";
}
if (pkg.bin !== undefined) return "server";
const hintSet = mergeHints(SERVER_DEP_HINTS, extraHints);
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
for (const key of Object.keys(allDeps)) {
if (hintSet.has(key)) return "server";
}
return "universal";
}
function mergeHints(
base: ReadonlySet<string>,
extra: ReadonlySet<string> | undefined
): ReadonlySet<string> {
if (extra === undefined || extra.size === 0) return base;
const merged = new Set(base);
for (const v of extra) merged.add(v);
return merged;
}

View File

@@ -0,0 +1,84 @@
# `types/types.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares the type-level vocabulary of `@suaveplan/overview`: the closed `Tier`
union, the `PackageRecord` row, the `PluginSummary` aggregate, the
`SuavePlanManifest` snapshot, the `WorkspaceGraph` adjacency structure, and the
`RawPackageJson` narrowing of the on-disk `package.json` fields that influence
tier inference.
Every other module in the package consumes these shapes. There is no runtime
code in this file — only `export type` / `export interface` declarations and
the `TIERS` constant array used for stable iteration order across roll-ups and
tests.
## Features
- Closed `Tier` union with documented priority order
- `TIERS` ordered tuple for deterministic iteration in roll-ups and tests
- `readonly`-deep `PackageRecord` shape — no accidental mutation downstream
- `PluginSummary` matches the columns produced by
`scripts/folder-reorg/generate-manifest.ts`
- `SuavePlanManifest` carries every aggregate that the Markdown manifest emits
- `WorkspaceGraph` exposes both forward + reverse adjacency for O(1) traversal
- `RawPackageJson` narrows `package.json` to only the fields the package reads
## Usage: Basic
```ts
import type { PackageRecord, Tier } from "@suaveplan/overview";
const record: PackageRecord = {
name: "@suaveplan/x",
short: "x",
category: "foundation",
plugin: "core",
dirName: "x",
version: "0.1.0",
description: "",
tier: "universal",
workspaceDeps: [],
hasBin: false,
hasBrowserField: false,
};
```
## Usage: Advanced
```ts
import { TIERS, type SuavePlanManifest, type Tier } from "@suaveplan/overview";
function totalCount(manifest: SuavePlanManifest): number {
return TIERS.reduce<number>(
(acc, tier: Tier) => acc + manifest.tierCounts[tier],
0,
);
}
```
`TIERS` is ordered for human-readable output (universal first, python last);
runtime code that needs the closed enum but is happy with any order can iterate
`Object.keys(manifest.tierCounts) as Tier[]`.
## API Reference
- `type Tier = "python" | "r3f" | "react" | "browser" | "server" | "universal"`
- `const TIERS: readonly Tier[]` — fixed ordering
- `interface PackageRecord` — `readonly` row, eleven fields
- `interface PluginSummary` — `{category, plugin, packages, workspaceDeps}`
- `interface SuavePlanManifest` — `{packages, plugins, categories, tierCounts}`
- `interface WorkspaceGraph` — `{nodes, forward, reverse}`
- `interface RawPackageJson` — only the fields read by `inferTier`
## Implementation Notes
Types live in a sibling `.types.ts` is not appropriate here because this file
defines the cross-module vocabulary rather than the types of a single feature
implementation. It is referenced directly by `src/index.ts` and every other
feature folder. The `readonly` modifiers cascade so downstream consumers cannot
accidentally mutate manifests they receive; mutation must be a conscious clone.

View File

@@ -0,0 +1,113 @@
/**
* Closed enumeration of tier labels recognised by the manifest, in the
* canonical priority order documented at
* `openspec/specs/monorepo-structure/spec.md`.
*
* Inference is done in `inferTier`: python → r3f → react → browser → server
* → universal.
*/
export type Tier =
| "python"
| "r3f"
| "react"
| "browser"
| "server"
| "universal";
/**
* Every tier in deterministic order. Useful for iterating roll-ups and for
* constructing all-tier empty buckets.
*/
export const TIERS: readonly Tier[] = [
"universal",
"browser",
"react",
"r3f",
"server",
"python",
] as const;
/**
* One row of the monorepo manifest. The minimum useful surface a downstream
* tool needs to render dashboards, graph dependencies, route imports per
* tier, or build deployment plans.
*/
export interface PackageRecord {
/** Full npm name, e.g. `"@suaveplan/overview"`. */
readonly name: string;
/** npm name without the `@suaveplan/` prefix. */
readonly short: string;
/** Top-level super-category folder under `packages/`. */
readonly category: string;
/** Mid-level plugin folder under `packages/<category>/`. */
readonly plugin: string;
/** Last path segment — the on-disk folder name of the package. */
readonly dirName: string;
/** `package.json` `version` field. */
readonly version: string;
/** `package.json` `description` field. Empty string when absent. */
readonly description: string;
/** Inferred tier — never observed from disk, always computed. */
readonly tier: Tier;
/** Sorted, deduped `@suaveplan/*` names referenced via `workspace:`. */
readonly workspaceDeps: readonly string[];
/** `true` when the package declares a `bin` field. */
readonly hasBin: boolean;
/** `true` when the package declares a `browser` field. */
readonly hasBrowserField: boolean;
}
/**
* Per-plugin aggregate row. Mirrors the Plugins table in `MANIFEST.md`.
*/
export interface PluginSummary {
readonly category: string;
readonly plugin: string;
/** Number of packages discovered for this `(category, plugin)` pair. */
readonly packages: number;
/** Total outgoing workspace edges from packages in this plugin. */
readonly workspaceDeps: number;
}
/**
* Full validated manifest snapshot. The shape `SuavePlanManifestSchema`
* parses to; what every public entry point returns.
*/
export interface SuavePlanManifest {
readonly packages: readonly PackageRecord[];
readonly plugins: readonly PluginSummary[];
readonly categories: readonly string[];
readonly tierCounts: Readonly<Record<Tier, number>>;
}
/**
* Adjacency structure derived from a list of `PackageRecord`s. Edges only
* connect known nodes — orphan dep references are dropped at build time so
* partial-migration manifests stay self-consistent.
*/
export interface WorkspaceGraph {
/** Every node name. */
readonly nodes: ReadonlySet<string>;
/** `pkg → its workspace dependencies (known nodes only)`. */
readonly forward: ReadonlyMap<string, ReadonlySet<string>>;
/** `pkg → its dependents (known nodes only)`. */
readonly reverse: ReadonlyMap<string, ReadonlySet<string>>;
}
/**
* Raw `package.json` shape consumed by `inferTier`. Only the fields that
* influence tier inference or dep discovery are present.
*/
export interface RawPackageJson {
readonly name?: string;
readonly version?: string;
readonly description?: string;
readonly dependencies?: Readonly<Record<string, string>>;
readonly devDependencies?: Readonly<Record<string, string>>;
readonly peerDependencies?: Readonly<Record<string, string>>;
readonly optionalDependencies?: Readonly<Record<string, string>>;
readonly browser?: unknown;
readonly bin?: unknown;
readonly engines?: Readonly<Record<string, string>>;
readonly python?: unknown;
}

View File

@@ -0,0 +1 @@
export {};

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,38 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/overview",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@suaveplan/error",
/^@suaveplan\/error\//,
"@suaveplan/types",
"tslib",
"zod",
"@opentelemetry/api",
/^node:/,
],
},
},
});

View File

@@ -0,0 +1,29 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
setupFiles: ["./tests/setup/polyfills.ts"],
include: ["src/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.types.ts",
"src/**/index.ts",
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
testTimeout: 30000,
},
});