Files
2026-05-27 23:29:53 +01:00

247 lines
10 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: overview (overview)
**Package**: `@suaveplan/overview`
**Status**: PROPOSED
> 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`