Files
suaveplan/openspec/archive/2026-05-25-enterprise-docs/design.md
2026-05-27 23:29:53 +01:00

6.5 KiB

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):

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:

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)

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

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:

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.