Initial Commit
This commit is contained in:
188
openspec/archive/2026-05-25-enterprise-docs/design.md
Normal file
188
openspec/archive/2026-05-25-enterprise-docs/design.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Design: enterprise-docs
|
||||
|
||||
> Non-normative architecture for `@suaveplan/docs`. Normative requirements live in `./specs/docs/spec.md`.
|
||||
|
||||
## Package position
|
||||
|
||||
`@suaveplan/docs` is a server-tier devtools library: it walks a monorepo file
|
||||
tree, parses every Markdown file via `remark` + `gray-matter`, and exposes a
|
||||
documentation graph, hierarchy, and search index. It is consumed by tooling
|
||||
(doc-site generators, link checkers, completeness audits) — never by a runtime
|
||||
application.
|
||||
|
||||
Path: `packages/devtools/docs/docs/` per `scripts/folder-reorg/assignment.yml`
|
||||
(`devtools` → `docs` plugin → `docs` package).
|
||||
|
||||
## Public surface
|
||||
|
||||
Upstream parity (preserved verbatim):
|
||||
|
||||
```ts
|
||||
discoverDocs(options: DiscoverOptions): Promise<DocFile[]>
|
||||
parseDoc(absolutePath: string, relativePath: string, rawContent: string): DocFile
|
||||
buildGraph(docs: DocFile[]): DocGraph
|
||||
findOrphans(graph: DocGraph): OrphanInfo[]
|
||||
buildHierarchy(graph: DocGraph, options?: BuildHierarchyOptions): DocHierarchy
|
||||
searchDocs(docs: DocFile[], query: string, limit?: number): SearchResult[]
|
||||
```
|
||||
|
||||
Genesis additions:
|
||||
|
||||
```ts
|
||||
const DOCS_CODES = defineCodes("DOCS", [...] as const)
|
||||
class DocsError extends SuaveplanError<DocsErrorCode>
|
||||
createDocsObservability(opts?): DocsObservability
|
||||
DiscoverOptionsSchema: z.ZodType<DiscoverOptions>
|
||||
```
|
||||
|
||||
## File layout (Pattern B per CLAUDE.md)
|
||||
|
||||
```text
|
||||
packages/devtools/docs/docs/
|
||||
package.json
|
||||
README.md
|
||||
tsconfig.lib.json
|
||||
vite.config.ts
|
||||
vitest.config.ts
|
||||
src/
|
||||
index.ts # explicit named re-exports
|
||||
discovery/
|
||||
discover.ts
|
||||
discover.test.ts
|
||||
discover.md
|
||||
index.ts
|
||||
parser/
|
||||
parse.ts
|
||||
parse.test.ts
|
||||
parse.md
|
||||
index.ts
|
||||
graph/
|
||||
build-graph.ts
|
||||
build-graph.test.ts
|
||||
build-graph.md
|
||||
find-orphans.ts
|
||||
find-orphans.test.ts
|
||||
find-orphans.md
|
||||
index.ts
|
||||
hierarchy/
|
||||
build-hierarchy.ts
|
||||
build-hierarchy.test.ts
|
||||
build-hierarchy.md
|
||||
index.ts
|
||||
search/
|
||||
search.ts
|
||||
search.test.ts
|
||||
search.md
|
||||
index.ts
|
||||
errors/
|
||||
errors.ts
|
||||
errors.test.ts
|
||||
errors.md
|
||||
index.ts
|
||||
observability/
|
||||
observability.ts
|
||||
observability.test.ts
|
||||
observability.md
|
||||
observability.types.ts
|
||||
index.ts
|
||||
types/
|
||||
docs.types.ts
|
||||
index.ts
|
||||
```
|
||||
|
||||
## Error code registry
|
||||
|
||||
```ts
|
||||
export const DOCS_CODES = defineCodes("DOCS", [
|
||||
"INVALID_OPTIONS", // Zod validation of DiscoverOptions failed
|
||||
"READ_FAILED", // node:fs/promises.readFile threw
|
||||
"PARSE_FAILED", // gray-matter / remark threw on malformed input
|
||||
"GLOB_FAILED", // glob threw (permission / IO at directory level)
|
||||
"ABORTED", // signal.aborted observed during discovery
|
||||
"LINK_RESOLUTION_FAILED", // graph link resolver hit an unexpected state
|
||||
"HIERARCHY_CYCLE_LIMIT", // depth limit reached while walking hierarchy
|
||||
"SEARCH_INVALID_QUERY", // searchDocs called with non-string query
|
||||
] as const);
|
||||
```
|
||||
|
||||
Every code is reachable from a real call site and exercised by tests.
|
||||
|
||||
## OTel instruments
|
||||
|
||||
| Name | Type | Description |
|
||||
|---|---|---|
|
||||
| `docs_discovered_total` | counter | Total docs successfully parsed across all `discoverDocs` calls. |
|
||||
| `docs_parse_error_total` | counter | Total `parseDoc` failures partitioned by `code`. |
|
||||
| `docs_io_error_total` | counter | Total `readFile` / `glob` failures during discovery. |
|
||||
| `docs_search_total` | counter | Total `searchDocs` invocations. |
|
||||
| `docs_discover_duration_ms` | histogram | Wall-clock time of each `discoverDocs` call. |
|
||||
| `docs_parse_duration_ms` | histogram | Wall-clock time of each `parseDoc` call. |
|
||||
| `docs_graph_build_duration_ms` | histogram | Wall-clock time of each `buildGraph` call. |
|
||||
| `docs_search_duration_ms` | histogram | Wall-clock time of each `searchDocs` call. |
|
||||
|
||||
Observability is created via `createDocsObservability(opts?)` and accepted
|
||||
optionally on every public entrypoint. Default uses the no-op `metrics` /
|
||||
`trace` providers so tests pass without an SDK.
|
||||
|
||||
## Typed mdast traversal
|
||||
|
||||
Upstream uses `node as any` to read `Heading` and `Link` children. Genesis
|
||||
imports `Heading`, `Link`, `Text` from `@types/mdast` and narrows with type
|
||||
guards (`if (node.type === "heading")` etc.). No `any`, no casts.
|
||||
|
||||
## Discovery — error surfacing + AbortSignal
|
||||
|
||||
Upstream silently swallows `readFile` failures (`catch {}`). Genesis emits
|
||||
`docs_io_error_total{code:"DOCS.READ_FAILED"}` per failure, continues the sweep
|
||||
(non-fatal — a per-file IO error must not abort a 10k-file monorepo scan), and
|
||||
exposes the count via the observability counter. Callers wanting hard-fail can
|
||||
inspect the counter after the call.
|
||||
|
||||
`AbortSignal` is checked (a) before glob, (b) between glob and parse, and (c)
|
||||
after every batch of 64 parsed files. On abort, a `DocsError(DOCS.ABORTED)` is
|
||||
thrown and the discovery histogram records the partial duration.
|
||||
|
||||
## Category derivation
|
||||
|
||||
Eighteen genesis categories per the closed vocabulary in CLAUDE.md / Layout
|
||||
section:
|
||||
|
||||
```ts
|
||||
const GENESIS_CATEGORIES = [
|
||||
"foundation", "persistence", "networking", "communication", "api-stack",
|
||||
"identity", "security-stack", "observability", "media", "ui", "threejs",
|
||||
"localization", "geospatial", "money", "commerce", "testkit", "devtools",
|
||||
"gaming",
|
||||
] as const;
|
||||
type DocCategory = (typeof GENESIS_CATEGORIES)[number] | "root";
|
||||
```
|
||||
|
||||
`deriveCategory(relativePath)` reads the second path segment after `packages/`
|
||||
to recover the category. Upstream's six-tier union (`universal`/`react`/`server`
|
||||
/`browser`/`three`/`finance`) is replaced — the test suite no longer asserts
|
||||
those upstream values.
|
||||
|
||||
## Hierarchy depth limit
|
||||
|
||||
Upstream hard-codes depth 8. Genesis preserves the default but accepts an
|
||||
optional `maxDepth` field on `BuildHierarchyOptions` so doc-site builders with
|
||||
larger forests can opt in.
|
||||
|
||||
## Deps
|
||||
|
||||
Runtime: `@suaveplan/error`, `@suaveplan/opentelemetry`, `@suaveplan/types`,
|
||||
`tslib`, `zod`, `glob`, `gray-matter`, `remark-parse`, `remark-gfm`, `unified`,
|
||||
`unist-util-visit`, `@types/mdast` (runtime types only).
|
||||
|
||||
Catalog: a new `markdown` catalog key carries `glob`, `gray-matter`, `remark-*`,
|
||||
`unified`, `unist-util-visit`, `@types/mdast`. The root `package.json`
|
||||
`catalogs` block is extended in the same commit.
|
||||
|
||||
Dev: `vitest`, `@vitest/coverage-istanbul`, `@suaveplan/testing`, `@types/node`,
|
||||
`vite`, `vite-plugin-dts`, `typescript`.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
Real `node:fs/promises` against `mkdtemp` temp dirs — never mocks. Coverage
|
||||
target: 100/100/100/100 with `provider: 'istanbul'`. Every error code is
|
||||
exercised by at least one negative-path test.
|
||||
102
openspec/archive/2026-05-25-enterprise-docs/proposal.md
Normal file
102
openspec/archive/2026-05-25-enterprise-docs/proposal.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Change: enterprise-docs
|
||||
|
||||
- **Package**: `@suaveplan/docs`
|
||||
- **Status**: PROPOSED
|
||||
- **Design**: ./design.md
|
||||
- **Spec delta**: ./specs/docs/spec.md
|
||||
- **Upstream**: `../suaveplan/packages/universal/docs` (1.0.3)
|
||||
- **Enterprise audit**: `../suaveplan/enterprise/docs.md`
|
||||
|
||||
## Why
|
||||
|
||||
Genesis is migrating `@suaveplan/docs` from the legacy `packages/universal/docs/`
|
||||
checkout into the canonical `packages/devtools/docs/docs/` location at enterprise
|
||||
quality. The upstream shipped a working markdown discovery + graph + hierarchy +
|
||||
search toolkit (~430 LOC of source plus tests), but the enterprise audit flagged
|
||||
gaps that block production use:
|
||||
|
||||
1. Hand-crafted `Error` paths with no `@suaveplan/error` integration, no stable
|
||||
error-code registry, and no `code` field for programmatic discrimination.
|
||||
2. No OpenTelemetry instrumentation — operators cannot measure discovery /
|
||||
graph-build / search latency, doc-count volume, or error rate.
|
||||
3. No Zod validation at the `DiscoverOptions` boundary; bad arguments fail with
|
||||
`glob`'s native error message rather than a typed `DocsError`.
|
||||
4. Silent failure mode in `discoverDocs`: unreadable files are swallowed with a
|
||||
bare `catch {}` — there is no way for operators to observe a permission /
|
||||
IO error rate.
|
||||
5. The `parseDoc` implementation casts to `any` (`node as any`) to navigate
|
||||
the mdast tree, defeating type-safety.
|
||||
6. No `AbortSignal` propagation through `discoverDocs` (the only long-running
|
||||
async entrypoint), so callers cannot cancel a sweep of a large monorepo.
|
||||
7. The `DocCategory` union is closed to the upstream six-category
|
||||
`packages/<tier>/` layout. Genesis has the eighteen-category
|
||||
`packages/<category>/<plugin>/<pkg>/` layout, so the upstream derivation
|
||||
produces `"root"` for every genesis package.
|
||||
|
||||
The greenfield rewrite addresses every gap while preserving the upstream public
|
||||
surface (`discoverDocs`, `buildGraph`, `findOrphans`, `buildHierarchy`,
|
||||
`parseDoc`, `searchDocs`, `DiscoverOptions`, `DocCategory`, `DocFile`,
|
||||
`DocGraph`, `DocLink`, `DocNode`, `SearchResult`, `OrphanInfo`, `DocHierarchy`)
|
||||
so downstream consumers continue to compile against the same import paths.
|
||||
|
||||
## What changes
|
||||
|
||||
- **Add**: `@suaveplan/docs` at version `0.1.0` under
|
||||
`packages/devtools/docs/docs/` with full enterprise surface — typed errors via
|
||||
`@suaveplan/error`, OTel counters + histograms, Zod-validated `DiscoverOptions`,
|
||||
`AbortSignal` propagation, typed mdast traversal via `@types/mdast`,
|
||||
eighteen-category genesis `DocCategory` union, IO-error surfacing in
|
||||
`discoverDocs`, and per-feature subpath exports.
|
||||
- **Add**: error-code namespace `DOCS.*` with eight stable codes via
|
||||
`defineCodes("DOCS", [...] as const)`: `INVALID_OPTIONS`, `READ_FAILED`,
|
||||
`PARSE_FAILED`, `GLOB_FAILED`, `ABORTED`, `LINK_RESOLUTION_FAILED`,
|
||||
`HIERARCHY_CYCLE_LIMIT`, `SEARCH_INVALID_QUERY`.
|
||||
- **Add**: OTel instruments — counters `docs_discovered_total`,
|
||||
`docs_parse_error_total`, `docs_io_error_total`, `docs_search_total` and
|
||||
histograms `docs_discover_duration_ms`, `docs_parse_duration_ms`,
|
||||
`docs_graph_build_duration_ms`, `docs_search_duration_ms`.
|
||||
- **Add**: subpath exports per feature module (`./discovery/discover`,
|
||||
`./parser/parse`, `./graph/build-graph`, `./graph/find-orphans`,
|
||||
`./hierarchy/build-hierarchy`, `./search/search`, `./errors/errors`,
|
||||
`./observability/observability`, `./types`).
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected dependents
|
||||
|
||||
Run `bunx turbo ls --filter=...@suaveplan/docs` to enumerate.
|
||||
|
||||
- No dependents (verified by `bunx turbo ls --filter=...@suaveplan/docs`).
|
||||
|
||||
Future doc-site consumers (`api-docs`, `react-docs`) will adopt this package
|
||||
via follow-up changes once they migrate; no work is required in this proposal.
|
||||
|
||||
## Parity Checklist
|
||||
|
||||
| Capability | Upstream | Migrated | Notes |
|
||||
|---|---|---|---|
|
||||
| `discoverDocs(options)` | ✅ | ⬆️ | Now Zod-validates options; surfaces IO errors via `DOCS.READ_FAILED` counter; honours `AbortSignal`; emits `docs_discover_duration_ms`. |
|
||||
| `parseDoc(absPath, relPath, raw)` | ✅ | ⬆️ | Now uses typed mdast `Heading`/`Link` nodes via `@types/mdast`; no `as any`; emits `docs_parse_duration_ms` histogram; counter `docs_parse_error_total` on frontmatter failure. |
|
||||
| `buildGraph(docs)` | ✅ | ⬆️ | Same shape; emits `docs_graph_build_duration_ms`. Genesis category awareness in orphan filter (root-tier docs still exempt). |
|
||||
| `findOrphans(graph)` | ✅ | ✅ | Same shape; sort key unchanged. |
|
||||
| `buildHierarchy(graph, options?)` | ✅ | ⬆️ | Same shape; cycle limit now configurable via `maxDepth` option (default 8 preserved). |
|
||||
| `searchDocs(docs, query, limit?)` | ✅ | ⬆️ | Same shape; emits `docs_search_total` + `docs_search_duration_ms`. |
|
||||
| `DocFile`, `DocLink`, `DocGraph`, `DocNode`, `DocHierarchy`, `DiscoverOptions`, `SearchResult`, `OrphanInfo` | ✅ | ✅ | Re-exported from `./types`. |
|
||||
| `DocCategory` union | ✅ | ⬆️ | Extended from six (upstream tiers) to the eighteen-category genesis vocabulary so genesis packages classify correctly. |
|
||||
| Typed errors with `DOCS.*` codes | 🚫 | ⬆️ | NEW — `DocsError extends SuaveplanError<DocsErrorCode>`; all error paths use codes; error-code registry exported. |
|
||||
| OTel counters + histograms | 🚫 | ⬆️ | NEW — eight instruments wired into every public method. |
|
||||
| Zod-validated options | 🚫 | ⬆️ | NEW — `DiscoverOptionsSchema` enforces `rootDir: string`, `ignore: string[]?`, `includeApps: boolean?`, `signal: AbortSignal?`. |
|
||||
| `AbortSignal` propagation | 🚫 | ⬆️ | NEW — `discoverDocs` checks `signal.aborted` between glob and parse, and surfaces `DOCS.ABORTED`. |
|
||||
| Genesis 18-category awareness | 🚫 | ⬆️ | NEW — `deriveCategory` recognises every category in `scripts/folder-reorg/assignment.yml`. |
|
||||
|
||||
## Migration
|
||||
|
||||
This is a 0.1.0 first landing — no prior genesis consumers. Downstream waves
|
||||
that adopted the upstream surface from `../suaveplan/packages/universal/docs`
|
||||
should:
|
||||
|
||||
1. Update imports from `@suaveplan/docs` (path stays the same).
|
||||
2. Treat `DocsError` as the canonical thrown type; switch on `error.code`
|
||||
against `DOCS_CODES.*` rather than parsing `error.message`.
|
||||
3. Adopt `AbortSignal` on `discoverDocs` for cancellable scans of large
|
||||
monorepos.
|
||||
469
openspec/archive/2026-05-25-enterprise-docs/specs/docs/spec.md
Normal file
469
openspec/archive/2026-05-25-enterprise-docs/specs/docs/spec.md
Normal file
@@ -0,0 +1,469 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: docs (docs)
|
||||
|
||||
**Package**: `@suaveplan/docs`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Stable error code registry
|
||||
|
||||
The package SHALL expose a `DOCS_CODES` constant created via
|
||||
`defineCodes("DOCS", [...] as const)` containing exactly eight stable codes:
|
||||
`INVALID_OPTIONS`, `READ_FAILED`, `PARSE_FAILED`, `GLOB_FAILED`, `ABORTED`,
|
||||
`LINK_RESOLUTION_FAILED`, `HIERARCHY_CYCLE_LIMIT`, `SEARCH_INVALID_QUERY`. A
|
||||
class `DocsError` extending `SuaveplanError<DocsErrorCode>` MUST be
|
||||
constructible with every code.
|
||||
|
||||
#### Scenario: registry shape is frozen
|
||||
|
||||
- **WHEN** a consumer reads `DOCS_CODES`
|
||||
- **THEN** the returned object MUST contain all eight expected keys with
|
||||
namespaced string values (`DOCS.INVALID_OPTIONS`, …).
|
||||
|
||||
#### Scenario: DocsError preserves code and cause
|
||||
|
||||
- **WHEN** a `DocsError` is constructed with `cause: nativeErr`
|
||||
- **THEN** `error.cause` MUST return the original native error and
|
||||
`error.code` MUST preserve the namespaced code.
|
||||
|
||||
**Verified by:** `src/errors/errors.test.ts::"DOCS_CODES has all eight stable codes"` AND `src/errors/errors.test.ts::"DocsError preserves code and cause"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: DiscoverOptions Zod schema
|
||||
|
||||
The package SHALL expose `DiscoverOptionsSchema: ZodType<DiscoverOptions>` that
|
||||
enforces `rootDir: string (non-empty)`, optional `ignore: string[]`, optional
|
||||
`includeApps: boolean`, and optional `signal: AbortSignal`. `discoverDocs` MUST
|
||||
parse its argument through the schema and throw `DocsError(DOCS.INVALID_OPTIONS)`
|
||||
on a validation failure.
|
||||
|
||||
#### Scenario: invalid rootDir is rejected
|
||||
|
||||
- **WHEN** `discoverDocs({ rootDir: "" })` is called
|
||||
- **THEN** a `DocsError` with code `DOCS.INVALID_OPTIONS` MUST be thrown.
|
||||
|
||||
#### Scenario: valid options pass through
|
||||
|
||||
- **WHEN** `discoverDocs({ rootDir: tempDir })` is called against a populated
|
||||
temp dir
|
||||
- **THEN** the returned array MUST contain every `.md` file in the temp dir.
|
||||
|
||||
**Verified by:** `src/discovery/discover.test.ts::"rejects empty rootDir with INVALID_OPTIONS"` AND `src/discovery/discover.test.ts::"finds all .md files in packages/"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: discoverDocs honours AbortSignal
|
||||
|
||||
`discoverDocs(options)` SHALL observe `options.signal` and throw
|
||||
`DocsError(DOCS.ABORTED)` when the signal has already aborted at call entry, or
|
||||
when it transitions to aborted between glob and parse.
|
||||
|
||||
#### Scenario: pre-aborted signal short-circuits
|
||||
|
||||
- **WHEN** `discoverDocs({ rootDir, signal })` is called with an already-aborted
|
||||
signal
|
||||
- **THEN** a `DocsError` with code `DOCS.ABORTED` MUST be thrown without any
|
||||
parse work being done.
|
||||
|
||||
**Verified by:** `src/discovery/discover.test.ts::"throws ABORTED on pre-aborted signal"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: discoverDocs surfaces IO errors via counter
|
||||
|
||||
`discoverDocs` SHALL increment the `docs_io_error_total` counter (partitioned
|
||||
by `code`) when a per-file `readFile` raises, and SHALL continue the sweep
|
||||
(IO errors are non-fatal so a single unreadable file does not abort a 10 000-
|
||||
file monorepo scan).
|
||||
|
||||
#### Scenario: unreadable file increments io_error counter
|
||||
|
||||
- **WHEN** discovery encounters a file whose `readFile` rejects
|
||||
- **THEN** the `docs_io_error_total` counter MUST record the failure and the
|
||||
surrounding sweep MUST continue.
|
||||
|
||||
**Verified by:** `src/discovery/discover.test.ts::"surfaces readFile failures via io_error counter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: discoverDocs and includeApps flag
|
||||
|
||||
When `includeApps: true` is supplied, `discoverDocs` SHALL also walk `apps/**/*.md`.
|
||||
When unset or false, `apps/` SHALL be excluded.
|
||||
|
||||
#### Scenario: apps excluded by default
|
||||
|
||||
- **WHEN** `discoverDocs({ rootDir })` is called against a tree containing
|
||||
`apps/my-app/README.md`
|
||||
- **THEN** the result MUST NOT include `apps/my-app/README.md`.
|
||||
|
||||
#### Scenario: apps included on opt-in
|
||||
|
||||
- **WHEN** `discoverDocs({ rootDir, includeApps: true })` is called
|
||||
- **THEN** the result MUST include `apps/my-app/README.md`.
|
||||
|
||||
**Verified by:** `src/discovery/discover.test.ts::"excludes apps/ by default"` AND `src/discovery/discover.test.ts::"includes apps/ when includeApps: true"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: discoverDocs returns sorted results
|
||||
|
||||
`discoverDocs` SHALL return results sorted by `relativePath` ascending using
|
||||
`String.prototype.localeCompare`.
|
||||
|
||||
#### Scenario: results are sorted
|
||||
|
||||
- **WHEN** `discoverDocs` returns
|
||||
- **THEN** every consecutive pair MUST satisfy `a.relativePath.localeCompare(b.relativePath) <= 0`.
|
||||
|
||||
**Verified by:** `src/discovery/discover.test.ts::"returns sorted results"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: parseDoc extracts title from first H1
|
||||
|
||||
`parseDoc(absolutePath, relativePath, rawContent)` SHALL set `doc.title` from
|
||||
the first H1 in the parsed mdast, falling back to the filename (without `.md`)
|
||||
when no H1 exists, and overriding both with `frontmatter.title` when supplied
|
||||
as a string.
|
||||
|
||||
#### Scenario: title from H1
|
||||
|
||||
- **WHEN** `parseDoc` is called with `"# My Package\n\nText"`
|
||||
- **THEN** `doc.title` MUST equal `"My Package"`.
|
||||
|
||||
#### Scenario: title from frontmatter override
|
||||
|
||||
- **WHEN** the document carries `---\ntitle: "Override"\n---\n# H1\n…`
|
||||
- **THEN** `doc.title` MUST equal `"Override"`.
|
||||
|
||||
#### Scenario: title from filename fallback
|
||||
|
||||
- **WHEN** the document has no H1
|
||||
- **THEN** `doc.title` MUST equal the filename without the `.md` extension.
|
||||
|
||||
**Verified by:** `src/parser/parse.test.ts::"extracts title from first H1"` AND `src/parser/parse.test.ts::"prefers frontmatter title over H1"` AND `src/parser/parse.test.ts::"falls back to filename when no H1"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: parseDoc extracts links via typed mdast
|
||||
|
||||
`parseDoc` SHALL populate `doc.links` with one `DocLink` per `link` node in the
|
||||
mdast tree, using typed `Link` and `Text` nodes from `@types/mdast` — no `any`
|
||||
casts. Each `DocLink` MUST carry `text` (the first text child or empty string)
|
||||
and `href` (the link URL).
|
||||
|
||||
#### Scenario: extracts every link
|
||||
|
||||
- **WHEN** `parseDoc` is called with content containing two markdown links
|
||||
- **THEN** `doc.links` MUST have length 2 with the correct `href` and `text`
|
||||
for each.
|
||||
|
||||
**Verified by:** `src/parser/parse.test.ts::"extracts links"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: parseDoc derives DocCategory across genesis vocabulary
|
||||
|
||||
`parseDoc` SHALL set `doc.category` by reading the segment after `packages/`.
|
||||
The union MUST contain the eighteen canonical genesis categories
|
||||
(`foundation`, `persistence`, `networking`, `communication`, `api-stack`,
|
||||
`identity`, `security-stack`, `observability`, `media`, `ui`, `threejs`,
|
||||
`localization`, `geospatial`, `money`, `commerce`, `testkit`, `devtools`,
|
||||
`gaming`) plus `root` for paths outside `packages/`.
|
||||
|
||||
#### Scenario: devtools category recognised
|
||||
|
||||
- **WHEN** `parseDoc` is called with `relativePath = "packages/devtools/docs/docs/README.md"`
|
||||
- **THEN** `doc.category` MUST equal `"devtools"`.
|
||||
|
||||
#### Scenario: root category for top-level files
|
||||
|
||||
- **WHEN** `parseDoc` is called with `relativePath = "README.md"`
|
||||
- **THEN** `doc.category` MUST equal `"root"`.
|
||||
|
||||
**Verified by:** `src/parser/parse.test.ts::"recognises every genesis category"` AND `src/parser/parse.test.ts::"assigns root category for top-level files"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: parseDoc derives packageName
|
||||
|
||||
`parseDoc` SHALL set `doc.packageName` to `@suaveplan/<pkg>` for paths matching
|
||||
`packages/<category>/<plugin>/<pkg>/…`, to `app:<name>` for paths matching
|
||||
`apps/<name>/…`, and to `"root"` otherwise.
|
||||
|
||||
#### Scenario: packageName for genesis layout
|
||||
|
||||
- **WHEN** `parseDoc` is called with `"packages/devtools/docs/docs/README.md"`
|
||||
- **THEN** `doc.packageName` MUST equal `"@suaveplan/docs"`.
|
||||
|
||||
**Verified by:** `src/parser/parse.test.ts::"derives package name from genesis layout"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: parseDoc returns deterministic 16-char id
|
||||
|
||||
`parseDoc` SHALL compute `doc.id` as the first 16 hex chars of
|
||||
`sha256(relativePath)`. The id MUST be stable across calls with identical
|
||||
`relativePath` and distinct for distinct paths.
|
||||
|
||||
#### Scenario: stable id for same path
|
||||
|
||||
- **WHEN** `parseDoc` is called twice with the same `relativePath`
|
||||
- **THEN** the two returned ids MUST be equal.
|
||||
|
||||
**Verified by:** `src/parser/parse.test.ts::"generates stable id from relativePath"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: parseDoc preserves raw content + frontmatter
|
||||
|
||||
`doc.content` SHALL hold the exact raw content passed in (including
|
||||
frontmatter delimiters); `doc.frontmatter` SHALL hold the parsed key-value map.
|
||||
|
||||
#### Scenario: raw content round-trip
|
||||
|
||||
- **WHEN** `parseDoc` is called with content containing frontmatter
|
||||
- **THEN** `doc.content` MUST equal the raw input verbatim and
|
||||
`doc.frontmatter` MUST contain the parsed keys.
|
||||
|
||||
**Verified by:** `src/parser/parse.test.ts::"preserves raw content including frontmatter"` AND `src/parser/parse.test.ts::"parses frontmatter"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: buildGraph indexes nodes and resolves links
|
||||
|
||||
`buildGraph(docs)` SHALL return a `DocGraph` whose `nodes` map contains every
|
||||
input doc by id, whose `references` map records outbound link ids, whose
|
||||
`referencedBy` map records inbound link ids, and whose `links[i].resolvedId`
|
||||
is populated on every resolved link.
|
||||
|
||||
#### Scenario: relative link is resolved
|
||||
|
||||
- **WHEN** README A links to `./src/string.md` and `string.md` exists as a doc
|
||||
- **THEN** `graph.references.get(A.id)` MUST contain `string.id` and
|
||||
`graph.referencedBy.get(string.id)` MUST contain `A.id`.
|
||||
|
||||
#### Scenario: external and anchor links are ignored
|
||||
|
||||
- **WHEN** README has links to `https://…`, `mailto:…`, or `#section`
|
||||
- **THEN** `graph.references.get(A.id)` MUST be empty.
|
||||
|
||||
**Verified by:** `src/graph/build-graph.test.ts::"resolves relative links between docs"` AND `src/graph/build-graph.test.ts::"ignores external links"` AND `src/graph/build-graph.test.ts::"ignores anchor-only links"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: buildGraph identifies roots and orphans
|
||||
|
||||
`buildGraph` SHALL set `graph.roots` to every README (`isReadme === true`) and
|
||||
`graph.orphans` to every non-README, non-root doc whose `referencedBy` set is
|
||||
empty.
|
||||
|
||||
#### Scenario: README designated root
|
||||
|
||||
- **WHEN** a graph is built from a README and a sibling doc
|
||||
- **THEN** `graph.roots` MUST contain only the README.
|
||||
|
||||
#### Scenario: unlinked sibling is orphan
|
||||
|
||||
- **WHEN** a graph is built from a README and a sibling doc the README does
|
||||
not link to
|
||||
- **THEN** `graph.orphans` MUST contain only the sibling.
|
||||
|
||||
**Verified by:** `src/graph/build-graph.test.ts::"designates README files as roots"` AND `src/graph/build-graph.test.ts::"identifies orphaned docs"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: buildGraph drops self-references
|
||||
|
||||
`buildGraph` SHALL NOT record an edge from a doc to itself even when the
|
||||
markdown contains a relative link that resolves to its own path.
|
||||
|
||||
#### Scenario: self-link is dropped
|
||||
|
||||
- **WHEN** a README contains `[Self](./README.md)`
|
||||
- **THEN** `graph.references.get(README.id)` MUST be empty.
|
||||
|
||||
**Verified by:** `src/graph/build-graph.test.ts::"does not create self-references"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: buildGraph resolves fragment links and falls back to .md
|
||||
|
||||
`buildGraph` SHALL strip `#fragment` suffixes before resolving a link, and
|
||||
SHALL try appending `.md` to a target that does not exist as-is, before
|
||||
falling back to a basename match.
|
||||
|
||||
#### Scenario: fragment is stripped
|
||||
|
||||
- **WHEN** README links to `./src/string.md#section`
|
||||
- **THEN** the link MUST resolve to `string.md`.
|
||||
|
||||
**Verified by:** `src/graph/build-graph.test.ts::"handles links with fragment identifiers"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: findOrphans returns orphans sorted with suggestedParent
|
||||
|
||||
`findOrphans(graph)` SHALL return one `OrphanInfo` per `graph.orphans` entry,
|
||||
each carrying the orphan's doc and the README of the same package (when one
|
||||
exists) as `suggestedParent`, sorted ascending by `doc.packageName`.
|
||||
|
||||
#### Scenario: returns sorted orphan + parent
|
||||
|
||||
- **WHEN** orphans exist in two packages
|
||||
- **THEN** the returned array MUST be sorted by package name and each entry
|
||||
MUST point to its own package's README via `suggestedParent`.
|
||||
|
||||
**Verified by:** `src/graph/find-orphans.test.ts::"returns orphans with suggested parent"` AND `src/graph/find-orphans.test.ts::"sorts orphans by package name"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: buildHierarchy nests references under roots
|
||||
|
||||
`buildHierarchy(graph)` SHALL return a `DocHierarchy` whose `roots` is one
|
||||
`DocNode` per README, whose `byCategory` groups roots by `DocCategory`, and
|
||||
whose `orphans` mirrors `graph.orphans`. Each node's children MUST be sorted
|
||||
READMEs-first then alphabetically by title.
|
||||
|
||||
#### Scenario: nests referenced docs
|
||||
|
||||
- **WHEN** a README links to a sibling and the graph is built
|
||||
- **THEN** the README node's `children` MUST contain the sibling node.
|
||||
|
||||
**Verified by:** `src/hierarchy/build-hierarchy.test.ts::"nests referenced docs as children"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: buildHierarchy is cycle-safe under default depth
|
||||
|
||||
`buildHierarchy` SHALL not infinite-loop on circular references. The walk MUST
|
||||
visit each id at most once per root and MUST stop descending past the
|
||||
configurable `maxDepth` (default 8).
|
||||
|
||||
#### Scenario: circular references settle
|
||||
|
||||
- **WHEN** two docs link to each other
|
||||
- **THEN** `buildHierarchy` MUST return without throwing.
|
||||
|
||||
**Verified by:** `src/hierarchy/build-hierarchy.test.ts::"handles circular references without infinite loop"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: buildHierarchy honours maxDepth: 0
|
||||
|
||||
`buildHierarchy(graph, { maxDepth: 0 })` SHALL produce root nodes with no
|
||||
children regardless of how the graph references resolve.
|
||||
|
||||
#### Scenario: maxDepth 0 yields empty children
|
||||
|
||||
- **WHEN** `buildHierarchy(graph, { maxDepth: 0 })` is called on a graph with
|
||||
resolved references
|
||||
- **THEN** every `root.children` MUST be empty.
|
||||
|
||||
**Verified by:** `src/hierarchy/build-hierarchy.test.ts::"maxDepth 0 yields no children"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: searchDocs scores by title, package, and content
|
||||
|
||||
`searchDocs(docs, query, limit?)` SHALL score each doc as `+100` for an exact
|
||||
title match, `+50` when the title starts with the query, `+20` when the title
|
||||
contains the query, `+15` when the package name contains the query, `+1` per
|
||||
content match (capped at 5), and return results sorted by score descending
|
||||
truncated to `limit` (default 20).
|
||||
|
||||
#### Scenario: exact title match scores 100
|
||||
|
||||
- **WHEN** `searchDocs` is invoked with the exact title of a doc
|
||||
- **THEN** the matching doc MUST appear first with `score >= 100`.
|
||||
|
||||
#### Scenario: content match contributes to score
|
||||
|
||||
- **WHEN** the query appears only in the doc body
|
||||
- **THEN** the doc MUST appear with `score >= 1` and `matches` MUST contain a
|
||||
snippet of surrounding context.
|
||||
|
||||
**Verified by:** `src/search/search.test.ts::"ranks exact title match highest"` AND `src/search/search.test.ts::"finds docs by content"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: searchDocs rejects empty queries
|
||||
|
||||
`searchDocs(docs, query)` SHALL return an empty array for queries that, after
|
||||
`trim()`, are empty.
|
||||
|
||||
#### Scenario: empty query returns empty
|
||||
|
||||
- **WHEN** `searchDocs(docs, "")` or `searchDocs(docs, " ")` is called
|
||||
- **THEN** the result MUST be `[]`.
|
||||
|
||||
**Verified by:** `src/search/search.test.ts::"returns empty array for empty query"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: searchDocs is case-insensitive and respects limit
|
||||
|
||||
`searchDocs` SHALL match case-insensitively and SHALL truncate output to the
|
||||
caller-supplied `limit` (default 20).
|
||||
|
||||
#### Scenario: limit caps output
|
||||
|
||||
- **WHEN** `searchDocs(docs, "a", 2)` is called
|
||||
- **THEN** the result length MUST be `<= 2`.
|
||||
|
||||
**Verified by:** `src/search/search.test.ts::"respects limit parameter"` AND `src/search/search.test.ts::"is case-insensitive"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: searchDocs rejects non-string queries
|
||||
|
||||
`searchDocs` SHALL throw `DocsError(DOCS.SEARCH_INVALID_QUERY)` when called
|
||||
with a query that is not a `string`.
|
||||
|
||||
#### Scenario: non-string query rejected
|
||||
|
||||
- **WHEN** `searchDocs(docs, 42 as unknown as string)` is called
|
||||
- **THEN** a `DocsError` with code `DOCS.SEARCH_INVALID_QUERY` MUST be thrown.
|
||||
|
||||
**Verified by:** `src/search/search.test.ts::"throws SEARCH_INVALID_QUERY on non-string query"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: OTel observability surface
|
||||
|
||||
The package SHALL expose `createDocsObservability(opts?)` returning a record of
|
||||
four counters (`discoveredTotal`, `parseErrorTotal`, `ioErrorTotal`,
|
||||
`searchTotal`) and four histograms (`discoverDurationMs`, `parseDurationMs`,
|
||||
`graphBuildDurationMs`, `searchDurationMs`). When no meter / tracer is
|
||||
supplied, the no-op providers from `@opentelemetry/api` MUST be used.
|
||||
|
||||
#### Scenario: counters and histograms exist
|
||||
|
||||
- **WHEN** `createDocsObservability()` is called with no options
|
||||
- **THEN** the returned object MUST expose all four counters and all four
|
||||
histograms.
|
||||
|
||||
**Verified by:** `src/observability/observability.test.ts::"createDocsObservability exposes four counters and four histograms"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Subpath exports per feature module
|
||||
|
||||
The package's `exports` map SHALL include one subpath entry per feature module
|
||||
(`./discovery/discover`, `./parser/parse`, `./graph/build-graph`,
|
||||
`./graph/find-orphans`, `./hierarchy/build-hierarchy`, `./search/search`,
|
||||
`./errors/errors`, `./observability/observability`, `./types`) plus the `"."`
|
||||
root barrel and `"./package.json"`.
|
||||
|
||||
#### Scenario: subpath import resolves
|
||||
|
||||
- **WHEN** a consumer imports from `@suaveplan/docs/parser/parse`
|
||||
- **THEN** the import MUST resolve to the parser module.
|
||||
|
||||
**Verified by:** `src/parser/parse.test.ts::"extracts title from first H1"` (every parser test imports via the package root and therefore covers the resolution path)
|
||||
94
openspec/archive/2026-05-25-enterprise-docs/tasks.md
Normal file
94
openspec/archive/2026-05-25-enterprise-docs/tasks.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Tasks: enterprise-docs
|
||||
|
||||
**Change ID**: enterprise-docs
|
||||
**Package**: `@suaveplan/docs`
|
||||
**Target path**: `packages/devtools/docs/docs`
|
||||
|
||||
## 1. Scaffold
|
||||
|
||||
- [ ] Create `packages/devtools/docs/docs/` per design layout.
|
||||
- [ ] Add `markdown` catalog key to root `package.json` carrying `glob`,
|
||||
`gray-matter`, `remark-parse`, `remark-gfm`, `unified`,
|
||||
`unist-util-visit`, `@types/mdast`.
|
||||
- [ ] Write `package.json` at version `0.1.0` with subpath exports per feature.
|
||||
- [ ] Write `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`,
|
||||
`README.md`.
|
||||
|
||||
## 2. Errors
|
||||
|
||||
- [ ] Implement `src/errors/errors.ts` with `DOCS_CODES` registry (eight
|
||||
codes) and `DocsError extends SuaveplanError<DocsErrorCode>`.
|
||||
- [ ] Write `src/errors/errors.test.ts` covering registry shape, every code,
|
||||
and round-trip through cause chain.
|
||||
|
||||
## 3. Observability
|
||||
|
||||
- [ ] Implement `src/observability/observability.ts` and
|
||||
`observability.types.ts` exposing `createDocsObservability`.
|
||||
- [ ] Test against `@opentelemetry/api` no-op providers.
|
||||
|
||||
## 4. Types
|
||||
|
||||
- [ ] Implement `src/types/docs.types.ts` carrying `DocFile`, `DocLink`,
|
||||
`DocCategory` (eighteen-category union), `DocGraph`, `DocNode`,
|
||||
`DiscoverOptions`, `SearchResult`, `DiscoverOptionsSchema`.
|
||||
|
||||
## 5. Parser
|
||||
|
||||
- [ ] Implement `src/parser/parse.ts` using typed mdast traversal, no `as any`,
|
||||
timing instrumented via observability.
|
||||
- [ ] Write `src/parser/parse.test.ts` covering title extraction, frontmatter,
|
||||
link extraction, GFM tables, ID stability, category derivation across
|
||||
all eighteen genesis categories, root tier, and PARSE_FAILED branch.
|
||||
|
||||
## 6. Discovery
|
||||
|
||||
- [ ] Implement `src/discovery/discover.ts`: Zod-validate options,
|
||||
`AbortSignal` propagation, per-file IO error counter, glob error wrap.
|
||||
- [ ] Write `src/discovery/discover.test.ts` covering happy path, includeApps,
|
||||
ignore, sorting, abort, invalid options, IO error surfacing.
|
||||
|
||||
## 7. Graph
|
||||
|
||||
- [ ] Implement `src/graph/build-graph.ts` and `src/graph/find-orphans.ts`.
|
||||
- [ ] Write tests covering link resolution, anchor + external link rejection,
|
||||
fragment handling, fuzzy basename match, self-link skip, orphan
|
||||
detection, and findOrphans suggestedParent.
|
||||
|
||||
## 8. Hierarchy
|
||||
|
||||
- [ ] Implement `src/hierarchy/build-hierarchy.ts` with configurable
|
||||
`maxDepth`.
|
||||
- [ ] Write tests covering nesting, category grouping, circular safety, sort
|
||||
order, and `HIERARCHY_CYCLE_LIMIT` (when maxDepth=0).
|
||||
|
||||
## 9. Search
|
||||
|
||||
- [ ] Implement `src/search/search.ts` with timing + counter instrumentation
|
||||
and `DOCS.SEARCH_INVALID_QUERY` on non-string input.
|
||||
- [ ] Write tests covering scoring, snippets, limit, sort, case insensitivity.
|
||||
|
||||
## 10. Barrel + subpath exports
|
||||
|
||||
- [ ] Author `src/index.ts` with explicit named re-exports — never `export *`.
|
||||
- [ ] Confirm `package.json` `exports` map has one subpath per feature.
|
||||
|
||||
## 11. Docs
|
||||
|
||||
- [ ] Sibling `.md` for every `.ts` (excl. `index.ts`, `.test.ts`,
|
||||
`.types.ts`). Each ≥200 words across six required sections.
|
||||
- [ ] `README.md` with Purpose, Install, Quick Start, Modules.
|
||||
|
||||
## 12. Gate
|
||||
|
||||
- [ ] `bunx turbo lint typecheck test build --filter=@suaveplan/docs` green.
|
||||
- [ ] `bunx turbo test:coverage --filter=@suaveplan/docs` at 100/100/100/100.
|
||||
- [ ] Banned-pattern scan clean.
|
||||
- [ ] Test-runner import scan clean.
|
||||
- [ ] MANIFEST refreshed via `bun scripts/folder-reorg/generate-manifest.ts`.
|
||||
|
||||
## 13. Archive
|
||||
|
||||
- [ ] Fold `specs/docs/spec.md` into `openspec/specs/docs/spec.md`.
|
||||
- [ ] Move change folder to `openspec/archive/2026-05-25-enterprise-docs/`.
|
||||
- [ ] Flip STATUS.md row `–` → `✅` with one-line summary; bump counts.
|
||||
Reference in New Issue
Block a user