247 lines
10 KiB
Markdown
247 lines
10 KiB
Markdown
<!-- markdownlint-disable MD024 -->
|
|
|
|
# Capability Spec Delta: overview (overview)
|
|
|
|
**Package**: `@suaveplan/overview`
|
|
**Status**: SHIPPED (2026-05-25)
|
|
|
|
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: Stable error registry
|
|
|
|
The package SHALL expose `OVERVIEW_CODES` registered via `defineCodes("OVERVIEW", [...])`
|
|
and a `OverviewError` class extending `@suaveplan/error`'s `SuaveplanError`. The
|
|
registry SHALL contain exactly six entries: `PARSE_FAILED`, `INVALID_RECORD`,
|
|
`CYCLE_DETECTED`, `UNKNOWN_PACKAGE`, `DISCOVERY_ABORTED`, `INVALID_TIER`. Each
|
|
code MUST be reachable from a real call site exercised by tests; no dead entries.
|
|
|
|
#### Scenario: error code uniqueness
|
|
|
|
- **WHEN** the test enumerates `Object.values(OVERVIEW_CODES)`
|
|
- **THEN** every value is a unique string in the form `"OVERVIEW.<NAME>"` and
|
|
`new OverviewError(code, "msg")` produces an instance whose `code` field equals
|
|
the input
|
|
|
|
#### Scenario: every code is thrown somewhere
|
|
|
|
- **WHEN** the test asserts each code appears as a `throw new OverviewError(...)`
|
|
argument in at least one `src/**/*.ts` file
|
|
- **THEN** the assertion passes — `PARSE_FAILED` from `discover`, `INVALID_RECORD`
|
|
from `discover`/`schemas`, `CYCLE_DETECTED` from `graph`, `UNKNOWN_PACKAGE` from
|
|
`graph`, `DISCOVERY_ABORTED` from `discover`, `INVALID_TIER` from `schemas`
|
|
|
|
**Verified by:** `src/errors/errors.test.ts::"every OVERVIEW code is unique"` AND `src/errors/errors.test.ts::"every code has a real call site in src/"`
|
|
|
|
---
|
|
|
|
### Requirement: Zod-validated data model
|
|
|
|
The package SHALL export Zod schemas `TierSchema`, `PackageRecordSchema`,
|
|
`PluginSummarySchema`, and `SuavePlanManifestSchema`, each paired with an
|
|
inferred TypeScript type. Schemas SHALL reject malformed inputs at the package
|
|
boundary and `INVALID_RECORD` SHALL be thrown when a discovered `package.json`
|
|
fails to parse against `PackageRecordSchema`.
|
|
|
|
#### Scenario: valid PackageRecord parses
|
|
|
|
- **WHEN** `PackageRecordSchema.safeParse({ name: "@suaveplan/x", short: "x",
|
|
category: "foundation", plugin: "core", dirName: "x", version: "0.1.0",
|
|
description: "d", tier: "universal", workspaceDeps: [], hasBin: false,
|
|
hasBrowserField: false })` runs
|
|
- **THEN** `success === true`
|
|
|
|
#### Scenario: malformed tier rejected
|
|
|
|
- **WHEN** `TierSchema.safeParse("rust")` runs
|
|
- **THEN** `success === false`
|
|
|
|
**Verified by:** `src/schemas/schemas.test.ts::"TierSchema accepts every tier"` AND `src/schemas/schemas.test.ts::"PackageRecordSchema rejects bad input per field"`
|
|
|
|
---
|
|
|
|
### Requirement: Tier inference
|
|
|
|
The package SHALL provide `inferTier(short, pkgJson, hints?)` that returns a
|
|
`Tier` using the priority order: python → r3f → react → browser → server →
|
|
universal. The hint set for the server-tier walk SHALL be exported as
|
|
`SERVER_DEP_HINTS` and SHALL be extensible per call via an optional
|
|
`hints: Set<string>` parameter.
|
|
|
|
#### Scenario: python wins
|
|
|
|
- **WHEN** `inferTier("python-foo", { python: {} })` runs
|
|
- **THEN** the return value is `"python"`
|
|
|
|
#### Scenario: server hint matches a Node-only dep
|
|
|
|
- **WHEN** `inferTier("x", { dependencies: { "node:fs": "*" } })` runs
|
|
- **THEN** the return value is `"server"`
|
|
|
|
#### Scenario: universal fallback
|
|
|
|
- **WHEN** `inferTier("x", {})` runs
|
|
- **THEN** the return value is `"universal"`
|
|
|
|
**Verified by:** `src/tier/tier.test.ts::"inferTier prioritises python > r3f > react > browser > server > universal"` AND `src/tier/tier.test.ts::"inferTier honours extension hints"`
|
|
|
|
---
|
|
|
|
### Requirement: Filesystem discovery (server sub-path)
|
|
|
|
The package SHALL expose `discoverPackages(options)` from the `./discover`
|
|
sub-path export. The function SHALL walk
|
|
`<root>/packages/<category>/<plugin>/<pkg>/package.json` exactly three folder
|
|
levels deep, parse each file as JSON, validate against `PackageRecordSchema`,
|
|
and return `SuavePlanManifest`. Invalid JSON SHALL produce `OVERVIEW.PARSE_FAILED`;
|
|
validation failure SHALL produce `OVERVIEW.INVALID_RECORD` (default behaviour:
|
|
throw; opt-in `onError: "skip"` collects errors into a returned array).
|
|
|
|
#### Scenario: temp-dir fixture discovers two packages
|
|
|
|
- **WHEN** the test creates a temp `packages/foundation/core/{a,b}/package.json`
|
|
fixture and calls `discoverPackages({ root: tmp })`
|
|
- **THEN** the returned manifest contains two records sorted by name
|
|
|
|
#### Scenario: invalid JSON throws PARSE_FAILED
|
|
|
|
- **WHEN** a fixture `package.json` is unparseable and `onError: "throw"`
|
|
- **THEN** an `OverviewError` with `code === "OVERVIEW.PARSE_FAILED"` is thrown
|
|
|
|
**Verified by:** `src/discover/discover.test.ts::"discovers a 3-level fixture"` AND `src/discover/discover.test.ts::"throws PARSE_FAILED on invalid JSON"` AND `src/discover/discover.test.ts::"throws INVALID_RECORD when fields missing"`
|
|
|
|
---
|
|
|
|
### Requirement: AbortSignal-aware async discovery
|
|
|
|
The package SHALL export `discoverPackagesIter(options)` returning an
|
|
`AsyncGenerator<PackageRecord>` that honours `options.signal`. On abort the
|
|
generator SHALL throw `OVERVIEW.DISCOVERY_ABORTED`.
|
|
|
|
#### Scenario: abort mid-walk
|
|
|
|
- **WHEN** a controller is aborted between the first and second yielded record
|
|
- **THEN** the second `.next()` rejects with `OverviewError` whose code is
|
|
`OVERVIEW.DISCOVERY_ABORTED`
|
|
|
|
**Verified by:** `src/discover/discover.test.ts::"discoverPackagesIter honours AbortSignal"`
|
|
|
|
---
|
|
|
|
### Requirement: Workspace graph utilities
|
|
|
|
The package SHALL export `buildGraph(records)`, `topologicalOrder(graph)`,
|
|
`detectCycles(graph)`, `transitiveDependencies(graph, pkg)`,
|
|
`transitiveDependents(graph, pkg)`, `leafPackages(graph)`, and
|
|
`rootPackages(graph)`. Graph edges SHALL only be recorded when both endpoints
|
|
are nodes (orphan dep names are dropped). Topological order SHALL throw
|
|
`OVERVIEW.CYCLE_DETECTED` carrying the offending cycle. Queries against
|
|
unknown packages SHALL throw `OVERVIEW.UNKNOWN_PACKAGE`.
|
|
|
|
#### Scenario: topo on a linear chain
|
|
|
|
- **WHEN** `topologicalOrder` runs over `a → b → c`
|
|
- **THEN** the result is `["c", "b", "a"]` (deps first)
|
|
|
|
#### Scenario: cycle detected
|
|
|
|
- **WHEN** `topologicalOrder` runs over `a → b → a`
|
|
- **THEN** an `OverviewError` with `code === "OVERVIEW.CYCLE_DETECTED"` is thrown
|
|
whose `context.cycle` contains both `a` and `b`
|
|
|
|
#### Scenario: transitive closure
|
|
|
|
- **WHEN** `transitiveDependencies(graph, "a")` runs over `a → b → c, b → d`
|
|
- **THEN** the result is `["b", "c", "d"]` sorted
|
|
|
|
#### Scenario: unknown package query
|
|
|
|
- **WHEN** `transitiveDependencies(graph, "missing")` runs
|
|
- **THEN** an `OverviewError` with `code === "OVERVIEW.UNKNOWN_PACKAGE"` is thrown
|
|
|
|
**Verified by:** `src/graph/graph.test.ts::"topologicalOrder respects dependency direction"` AND `src/graph/graph.test.ts::"topologicalOrder throws CYCLE_DETECTED on a cycle"` AND `src/graph/graph.test.ts::"transitiveDependencies BFS closure"` AND `src/graph/graph.test.ts::"transitiveDependencies throws UNKNOWN_PACKAGE"` AND `src/graph/graph.test.ts::"leaves and roots on a star"`
|
|
|
|
---
|
|
|
|
### Requirement: Roll-up aggregates
|
|
|
|
The package SHALL export `byPlugin(records)`, `byCategory(records)`, and
|
|
`byTier(records)`. `byPlugin` SHALL return `PluginSummary[]` sorted by
|
|
`"<category>/<plugin>"`. `byCategory` SHALL return `Record<string, number>`
|
|
of package counts. `byTier` SHALL return `Record<Tier, PackageRecord[]>` for
|
|
all six tiers (empty arrays when absent).
|
|
|
|
#### Scenario: byPlugin aggregates counts and dep totals
|
|
|
|
- **WHEN** `byPlugin` runs over three records across two `(category, plugin)`
|
|
pairs with workspace-dep counts `[2, 3, 1]` and the two pairs have `[2, 1]`
|
|
records respectively
|
|
- **THEN** the summary rows carry the expected `packages` and `workspaceDeps`
|
|
totals
|
|
|
|
#### Scenario: byTier covers all tiers
|
|
|
|
- **WHEN** `byTier` runs over an empty list
|
|
- **THEN** the returned record has every tier key with an empty array value
|
|
|
|
**Verified by:** `src/rollups/rollups.test.ts::"byPlugin aggregates packages + workspace deps"` AND `src/rollups/rollups.test.ts::"byCategory counts packages"` AND `src/rollups/rollups.test.ts::"byTier covers every Tier key"`
|
|
|
|
---
|
|
|
|
### Requirement: OpenTelemetry instrumentation
|
|
|
|
The package SHALL emit three counters and one histogram via
|
|
`metrics.getMeter("@suaveplan/overview", "0.1.0")`:
|
|
`overview.packages_discovered_total`,
|
|
`overview.records_rejected_total{reason}`, `overview.graph_builds_total`, and
|
|
`overview.discover_duration_ms`. Counters and histograms SHALL increment / record
|
|
during the normal API paths.
|
|
|
|
#### Scenario: counters move on discovery
|
|
|
|
- **WHEN** an in-memory metric reader is installed and `discoverPackages` runs
|
|
against a fixture
|
|
- **THEN** `overview.packages_discovered_total` increases by `records.length`
|
|
and `overview.discover_duration_ms` records one observation
|
|
|
|
**Verified by:** `src/telemetry/telemetry.test.ts::"discoverPackages emits counter + histogram"` AND `src/telemetry/telemetry.test.ts::"buildGraph emits graph_builds_total"`
|
|
|
|
---
|
|
|
|
### Requirement: Tier purity at the root entry
|
|
|
|
The package root entry SHALL NOT import `node:*` modules. Node-only behaviour
|
|
SHALL live exclusively under the `./discover` sub-path export so universal-tier
|
|
consumers (browser, R3F, edge) can import the root safely.
|
|
|
|
#### Scenario: root barrel has no node: imports
|
|
|
|
- **WHEN** the test parses `src/index.ts` and every transitive import not under
|
|
`src/discover/`
|
|
- **THEN** none of the resolved files contain `from "node:` or `require("node:`
|
|
literal strings
|
|
|
|
**Verified by:** `src/discover/discover.test.ts::"root barrel imports no node:* modules"`
|
|
|
|
---
|
|
|
|
### Requirement: Co-located docs and 100% coverage
|
|
|
|
Every non-barrel, non-test, non-types `.ts` file under `src/` SHALL have a
|
|
sibling `.md` documentation file matching the required-section template. The
|
|
package SHALL meet 100% statement, branch, function, and line coverage under
|
|
Istanbul.
|
|
|
|
#### Scenario: docs:check passes
|
|
|
|
- **WHEN** `bun run docs:check --filter=@suaveplan/overview` runs
|
|
- **THEN** the exit code is 0
|
|
|
|
#### Scenario: coverage gate
|
|
|
|
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/overview` runs
|
|
- **THEN** the Istanbul report shows 100/100/100/100
|
|
|
|
**Verified by:** `bunx turbo test:coverage --filter=@suaveplan/overview` AND `bun run docs:check --filter=@suaveplan/overview`
|